Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions webapp/api/auth/authConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL Node configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
*/

const authorityUrl = process.env.B2C_BASE_URL + '/B2C_1_login';

export const msalConfig = {
auth: {
clientId: process.env.B2C_CLIENT_ID,
authority: authorityUrl,
clientSecret: process.env.B2C_CLIENT_SECRET,
knownAuthorities: [new URL(authorityUrl).host],
protocolMode: 'OIDC',
},
system: {
loggerOptions: {
piiLoggingEnabled: false,
logLevel: 2,
loggerCallback(loglevel, message, containsPii) {
if (!containsPii) log.info(message);
},
},
},
};
267 changes: 267 additions & 0 deletions webapp/api/auth/authProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
const msal = require('@azure/msal-node');
const axios = require('axios');

class AuthProvider {
msalConfig;
cryptoProvider;

constructor(msalConfig) {
this.msalConfig = msalConfig
this.cryptoProvider = new msal.CryptoProvider();
};

login(options = {}) {
return async (req, res, next) => {

/**
* MSAL Node library allows you to pass your custom state as state parameter in the Request object.
* The state parameter can also be used to encode information of the app's state before redirect.
* You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
*/
const state = this.cryptoProvider.base64Encode(
JSON.stringify({
successRedirect: options.successRedirect || '/',
})
);

const authCodeUrlRequestParams = {
state: state,

/**
* By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
scopes: options.scopes || [],
redirectUri: options.redirectUri,
};

const authCodeRequestParams = {
state: state,

/**
* By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
scopes: options.scopes || [],
redirectUri: options.redirectUri,
};

/**
* If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will
* make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making
* metadata discovery calls, thereby improving performance of token acquisition process. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md
*/
if (!this.msalConfig.auth.cloudDiscoveryMetadata || !this.msalConfig.auth.authorityMetadata) {

const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([
this.getCloudDiscoveryMetadata(this.msalConfig.auth.authority),
this.getAuthorityMetadata(this.msalConfig.auth.authority)
]);

this.msalConfig.auth.cloudDiscoveryMetadata = JSON.stringify(cloudDiscoveryMetadata);
this.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
}

const msalInstance = this.getMsalInstance(this.msalConfig);

// trigger the first leg of auth code flow
return this.redirectToAuthCodeUrl(
authCodeUrlRequestParams,
authCodeRequestParams,
msalInstance
)(req, res, next);
};
}

acquireToken(options = {}) {
return async (req, res, next) => {
try {
const msalInstance = this.getMsalInstance(this.msalConfig);

/**
* If a token cache exists in the session, deserialize it and set it as the
* cache for the new MSAL CCA instance. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
*/
if (req.session.tokenCache) {
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
}

const tokenResponse = await msalInstance.acquireTokenSilent({
account: req.session.account,
scopes: options.scopes || [],
});

/**
* On successful token acquisition, write the updated token
* cache back to the session. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
*/
req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.accessToken = tokenResponse.accessToken;
req.session.idToken = tokenResponse.idToken;
req.session.account = tokenResponse.account;

res.redirect(options.successRedirect);
} catch (error) {
if (error instanceof msal.InteractionRequiredAuthError) {
return this.login({
scopes: options.scopes || [],
redirectUri: options.redirectUri,
successRedirect: options.successRedirect || '/',
})(req, res, next);
}

next(error);
}
};
}

handleRedirect(options = {}) {
return async (req, res, next) => {
if (!req.body || !req.body.state) {
return next(new Error('Error: response not found'));
}

const authCodeRequest = {
...req.session.authCodeRequest,
code: req.body.code,
codeVerifier: req.session.pkceCodes.verifier,
};

try {
const msalInstance = this.getMsalInstance(this.msalConfig);

if (req.session.tokenCache) {
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
}

const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);

req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.idToken = tokenResponse.idToken;
req.session.account = tokenResponse.account;
req.session.isAuthenticated = true;

const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state));
res.redirect(state.successRedirect);
} catch (error) {
next(error);
}
}
}

logout(options = {}) {
return (req, res, next) => {

/**
* Construct a logout URI and redirect the user to end the
* session with Azure AD. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
*/
let logoutUri = `${this.msalConfig.auth.authority}/oauth2/v2.0/`;

if (options.postLogoutRedirectUri) {
logoutUri += `logout?post_logout_redirect_uri=${options.postLogoutRedirectUri}`;
}

req.session.destroy(() => {
res.redirect(logoutUri);
});
}
}

/**
* Instantiates a new MSAL ConfidentialClientApplication object
* @returns
* @param msalConfig
*/
getMsalInstance(msalConfig) {
return new msal.ConfidentialClientApplication(msalConfig);
}


/**
* Prepares the auth code request parameters and initiates the first leg of auth code flow
* @param authCodeUrlRequestParams
* @param authCodeRequestParams
* @param msalInstance
*/
redirectToAuthCodeUrl(authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
return async (req, res, next) => {
// Generate PKCE Codes before starting the authorization flow
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();

// Set generated PKCE codes and method as session vars
req.session.pkceCodes = {
challengeMethod: 'S256',
verifier: verifier,
challenge: challenge,
};

/**
* By manipulating the request objects below before each request, we can obtain
* auth artifacts with desired claims. For more information, visit:
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationcoderequest
**/
req.session.authCodeUrlRequest = {
...authCodeUrlRequestParams,
responseMode: msal.ResponseMode.FORM_POST, // recommended for confidential clients
codeChallenge: req.session.pkceCodes.challenge,
codeChallengeMethod: req.session.pkceCodes.challengeMethod,
};

req.session.authCodeRequest = {
...authCodeRequestParams,
code: '',
};

try {
const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
res.redirect(authCodeUrlResponse);
} catch (error) {
next(error);
}
};
}

/**
* Retrieves cloud discovery metadata from the /discovery/instance endpoint
* @returns
*/
async getCloudDiscoveryMetadata(authority) {
const endpoint = 'https://login.microsoftonline.com/common/discovery/instance';

try {
const response = await axios.get(endpoint, {
params: {
'api-version': '1.1',
'authorization_endpoint': `${authority}/oauth2/v2.0/authorize`
}
});

return await response.data;
} catch (error) {
throw error;
}
}

/**
* Retrieves oidc metadata from the openid endpoint
* @returns
*/
async getAuthorityMetadata(authority) {
const endpoint = `${authority}/v2.0/.well-known/openid-configuration`;

try {
const response = await axios.get(endpoint);
return await response.data;
} catch (error) {
console.log(error);
}
}
}

module.exports = AuthProvider;
Loading
Loading