AuthProvider.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. const msal = require('@azure/msal-node');
  2. const axios = require('axios');
  3. const { msalConfig } = require('../authConfig');
  4. class AuthProvider {
  5. msalConfig;
  6. cryptoProvider;
  7. constructor(msalConfig) {
  8. this.msalConfig = msalConfig
  9. this.cryptoProvider = new msal.CryptoProvider();
  10. };
  11. login(options = {}) {
  12. return async (req, res, next) => {
  13. /**
  14. * MSAL Node library allows you to pass your custom state as state parameter in the Request object.
  15. * The state parameter can also be used to encode information of the app's state before redirect.
  16. * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
  17. */
  18. const state = this.cryptoProvider.base64Encode(
  19. JSON.stringify({
  20. successRedirect: options.successRedirect || '/',
  21. })
  22. );
  23. const authCodeUrlRequestParams = {
  24. state: state,
  25. /**
  26. * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit:
  27. * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
  28. */
  29. scopes: options.scopes || [],
  30. redirectUri: options.redirectUri,
  31. };
  32. const authCodeRequestParams = {
  33. state: state,
  34. /**
  35. * By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit:
  36. * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
  37. */
  38. scopes: options.scopes || [],
  39. redirectUri: options.redirectUri,
  40. };
  41. /**
  42. * If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will
  43. * make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making
  44. * metadata discovery calls, thereby improving performance of token acquisition process. For more, see:
  45. * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md
  46. */
  47. if (!this.msalConfig.auth.cloudDiscoveryMetadata || !this.msalConfig.auth.authorityMetadata) {
  48. const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([
  49. this.getCloudDiscoveryMetadata(this.msalConfig.auth.authority),
  50. this.getAuthorityMetadata(this.msalConfig.auth.authority)
  51. ]);
  52. this.msalConfig.auth.cloudDiscoveryMetadata = JSON.stringify(cloudDiscoveryMetadata);
  53. this.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
  54. }
  55. const msalInstance = this.getMsalInstance(this.msalConfig);
  56. console.log(msalInstance);
  57. // trigger the first leg of auth code flow
  58. return this.redirectToAuthCodeUrl(
  59. authCodeUrlRequestParams,
  60. authCodeRequestParams,
  61. msalInstance
  62. )(req, res, next);
  63. };
  64. }
  65. acquireToken(options = {}) {
  66. return async (req, res, next) => {
  67. try {
  68. const msalInstance = this.getMsalInstance(this.msalConfig);
  69. /**
  70. * If a token cache exists in the session, deserialize it and set it as the
  71. * cache for the new MSAL CCA instance. For more, see:
  72. * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
  73. */
  74. if (req.session.tokenCache) {
  75. msalInstance.getTokenCache().deserialize(req.session.tokenCache);
  76. }
  77. const tokenResponse = await msalInstance.acquireTokenSilent({
  78. account: req.session.account,
  79. scopes: options.scopes || req.body.scopes || [],
  80. });
  81. /**
  82. * On successful token acquisition, write the updated token
  83. * cache back to the session. For more, see:
  84. * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
  85. */
  86. req.session.tokenCache = msalInstance.getTokenCache().serialize();
  87. req.session.accessToken = tokenResponse.accessToken;
  88. req.session.idToken = tokenResponse.idToken;
  89. req.session.account = tokenResponse.account;
  90. req.session.apiUri = req.body.api_uri;
  91. req.session.param = req.body.param;
  92. res.redirect(options.successRedirect);
  93. } catch (error) {
  94. if (error instanceof msal.InteractionRequiredAuthError) {
  95. return this.login({
  96. scopes: options.scopes || [],
  97. redirectUri: options.redirectUri,
  98. successRedirect: options.successRedirect || '/',
  99. })(req, res, next);
  100. }
  101. next(error);
  102. }
  103. };
  104. }
  105. handleRedirect(options = {}) {
  106. return async (req, res, next) => {
  107. if (!req.body || !req.body.state) {
  108. return next(new Error('Error: response not found'));
  109. }
  110. if (!req.session || !req.session.pkceCodes) {
  111. return next(new Error('Error: session pkceCodes not found'));
  112. }
  113. const authCodeRequest = {
  114. ...req.session.authCodeRequest,
  115. code: req.body.code,
  116. codeVerifier: req.session.pkceCodes.verifier,
  117. };
  118. try {
  119. const msalInstance = this.getMsalInstance(this.msalConfig);
  120. if (req.session.tokenCache) {
  121. msalInstance.getTokenCache().deserialize(req.session.tokenCache);
  122. }
  123. const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
  124. req.session.tokenCache = msalInstance.getTokenCache().serialize();
  125. req.session.idToken = tokenResponse.idToken;
  126. req.session.account = tokenResponse.account;
  127. req.session.isAuthenticated = true;
  128. const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state));
  129. res.redirect(state.successRedirect);
  130. } catch (error) {
  131. next(error);
  132. }
  133. }
  134. }
  135. logout(options = {}) {
  136. return (req, res, next) => {
  137. /**
  138. * Construct a logout URI and redirect the user to end the
  139. * session with Azure AD. For more information, visit:
  140. * https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
  141. */
  142. let logoutUri = `${this.msalConfig.auth.authority}/oauth2/v2.0/`;
  143. if (options.postLogoutRedirectUri) {
  144. logoutUri += `logout?post_logout_redirect_uri=${options.postLogoutRedirectUri}`;
  145. }
  146. req.session.destroy(() => {
  147. res.redirect(logoutUri);
  148. });
  149. }
  150. }
  151. /**
  152. * Instantiates a new MSAL ConfidentialClientApplication object
  153. * @param msalConfig: MSAL Node Configuration object
  154. * @returns
  155. */
  156. getMsalInstance(msalConfig) {
  157. return new msal.ConfidentialClientApplication(msalConfig);
  158. }
  159. /**
  160. * Prepares the auth code request parameters and initiates the first leg of auth code flow
  161. * @param req: Express request object
  162. * @param res: Express response object
  163. * @param next: Express next function
  164. * @param authCodeUrlRequestParams: parameters for requesting an auth code url
  165. * @param authCodeRequestParams: parameters for requesting tokens using auth code
  166. */
  167. redirectToAuthCodeUrl(authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
  168. return async (req, res, next) => {
  169. // Generate PKCE Codes before starting the authorization flow
  170. const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
  171. // Set generated PKCE codes and method as session vars
  172. req.session.pkceCodes = {
  173. challengeMethod: 'S256',
  174. verifier: verifier,
  175. challenge: challenge,
  176. };
  177. /**
  178. * By manipulating the request objects below before each request, we can obtain
  179. * auth artifacts with desired claims. For more information, visit:
  180. * https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest
  181. * https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationcoderequest
  182. **/
  183. req.session.authCodeUrlRequest = {
  184. ...authCodeUrlRequestParams,
  185. responseMode: msal.ResponseMode.FORM_POST, // recommended for confidential clients
  186. codeChallenge: req.session.pkceCodes.challenge,
  187. codeChallengeMethod: req.session.pkceCodes.challengeMethod,
  188. };
  189. req.session.authCodeRequest = {
  190. ...authCodeRequestParams,
  191. code: '',
  192. };
  193. try {
  194. console.log(req.session);
  195. const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
  196. res.redirect(authCodeUrlResponse);
  197. } catch (error) {
  198. next(error);
  199. }
  200. };
  201. }
  202. /**
  203. * Retrieves cloud discovery metadata from the /discovery/instance endpoint
  204. * @returns
  205. */
  206. async getCloudDiscoveryMetadata(authority) {
  207. const endpoint = 'https://login.microsoftonline.com/common/discovery/instance';
  208. try {
  209. const response = await axios.get(endpoint, {
  210. params: {
  211. 'api-version': '1.1',
  212. 'authorization_endpoint': `${authority}/oauth2/v2.0/authorize`
  213. }
  214. });
  215. return await response.data;
  216. } catch (error) {
  217. throw error;
  218. }
  219. }
  220. /**
  221. * Retrieves oidc metadata from the openid endpoint
  222. * @returns
  223. */
  224. async getAuthorityMetadata(authority) {
  225. const endpoint = `${authority}/v2.0/.well-known/openid-configuration`;
  226. try {
  227. const response = await axios.get(endpoint);
  228. return await response.data;
  229. } catch (error) {
  230. console.log(error);
  231. }
  232. }
  233. }
  234. const authProvider = new AuthProvider(msalConfig);
  235. module.exports = authProvider;