From 5b185176830a1ab21788bb0784b214e5d989475c Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 7 May 2024 14:36:18 +0100 Subject: [PATCH] This sample app performs 2 things 1. Patch graph api, which helps to update user infromation using @me 2. MFA through conditional access which can be used to gate some sensitive action --- .../.vscode/extensions.json | 6 + .../7-sign-in-express-mfa/App/app.js | 68 + .../App/auth/AuthProvider.js | 246 ++++ .../7-sign-in-express-mfa/App/authConfig.js | 49 + .../App/controller/authController.js | 13 + .../7-sign-in-express-mfa/App/fetch.js | 33 + .../App/package-lock.json | 1141 +++++++++++++++++ .../7-sign-in-express-mfa/App/package.json | 21 + .../App/public/stylesheets/style.css | 8 + .../7-sign-in-express-mfa/App/routes/auth.js | 9 + .../7-sign-in-express-mfa/App/routes/index.js | 17 + .../7-sign-in-express-mfa/App/routes/users.js | 80 ++ .../7-sign-in-express-mfa/App/server.js | 90 ++ .../7-sign-in-express-mfa/App/views/error.hbs | 3 + .../7-sign-in-express-mfa/App/views/id.hbs | 16 + .../7-sign-in-express-mfa/App/views/index.hbs | 12 + .../App/views/layout.hbs | 13 + .../App/views/updateProfile.hbs | 26 + .../AppCreationScripts/AppCreationScripts.md | 138 ++ .../AppCreationScripts/Cleanup.ps1 | 152 +++ .../AppCreationScripts/Configure.ps1 | 352 +++++ .../AppCreationScripts/apps.json | 60 + .../AppCreationScripts/quickstart.md | 30 + .../AppCreationScripts/sample.json | 67 + .../README-use-certificate.md | 311 +++++ .../7-sign-in-express-mfa/README.md | 371 ++++++ .../admin-center-settings-icon.png | Bin 0 -> 818 bytes .../ReadmeFiles/screenshot.png | Bin 0 -> 12606 bytes .../ReadmeFiles/topology.png | Bin 0 -> 22933 bytes .../7-sign-in-express-mfa/ReadmeFiles/yes.png | Bin 0 -> 614 bytes .../7-sign-in-express-mfa/package-lock.json | 6 + 31 files changed, 3338 insertions(+) create mode 100644 1-Authentication/7-sign-in-express-mfa/.vscode/extensions.json create mode 100644 1-Authentication/7-sign-in-express-mfa/App/app.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/auth/AuthProvider.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/authConfig.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/controller/authController.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/fetch.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/package-lock.json create mode 100644 1-Authentication/7-sign-in-express-mfa/App/package.json create mode 100644 1-Authentication/7-sign-in-express-mfa/App/public/stylesheets/style.css create mode 100644 1-Authentication/7-sign-in-express-mfa/App/routes/auth.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/routes/index.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/routes/users.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/server.js create mode 100644 1-Authentication/7-sign-in-express-mfa/App/views/error.hbs create mode 100644 1-Authentication/7-sign-in-express-mfa/App/views/id.hbs create mode 100644 1-Authentication/7-sign-in-express-mfa/App/views/index.hbs create mode 100644 1-Authentication/7-sign-in-express-mfa/App/views/layout.hbs create mode 100644 1-Authentication/7-sign-in-express-mfa/App/views/updateProfile.hbs create mode 100644 1-Authentication/7-sign-in-express-mfa/AppCreationScripts/AppCreationScripts.md create mode 100644 1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Cleanup.ps1 create mode 100644 1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Configure.ps1 create mode 100644 1-Authentication/7-sign-in-express-mfa/AppCreationScripts/apps.json create mode 100644 1-Authentication/7-sign-in-express-mfa/AppCreationScripts/quickstart.md create mode 100644 1-Authentication/7-sign-in-express-mfa/AppCreationScripts/sample.json create mode 100644 1-Authentication/7-sign-in-express-mfa/README-use-certificate.md create mode 100644 1-Authentication/7-sign-in-express-mfa/README.md create mode 100644 1-Authentication/7-sign-in-express-mfa/ReadmeFiles/admin-center-settings-icon.png create mode 100644 1-Authentication/7-sign-in-express-mfa/ReadmeFiles/screenshot.png create mode 100644 1-Authentication/7-sign-in-express-mfa/ReadmeFiles/topology.png create mode 100644 1-Authentication/7-sign-in-express-mfa/ReadmeFiles/yes.png create mode 100644 1-Authentication/7-sign-in-express-mfa/package-lock.json diff --git a/1-Authentication/7-sign-in-express-mfa/.vscode/extensions.json b/1-Authentication/7-sign-in-express-mfa/.vscode/extensions.json new file mode 100644 index 000000000..ffb3679d8 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["ms-azuretools.ms-entra"] + } + \ No newline at end of file diff --git a/1-Authentication/7-sign-in-express-mfa/App/app.js b/1-Authentication/7-sign-in-express-mfa/App/app.js new file mode 100644 index 000000000..3a50080b2 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/app.js @@ -0,0 +1,68 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +require('dotenv').config(); + +var path = require('path'); +var express = require('express'); +var session = require('express-session'); +var createError = require('http-errors'); +var cookieParser = require('cookie-parser'); +var logger = require('morgan'); + +var indexRouter = require('./routes/index'); +var usersRouter = require('./routes/users'); +var authRouter = require('./routes/auth'); + +// initialize express +var app = express(); + +/** + * Using express-session middleware for persistent user session. Be sure to + * familiarize yourself with available options. Visit: https://www.npmjs.com/package/express-session + */ +app.use( + session({ + secret: process.env.EXPRESS_SESSION_SECRET || 'Enter_the_Express_Session_Secret_Here', + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + secure: false, // set this to true on production + }, + }) +); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'hbs'); + +app.use(logger('dev')); +app.use(express.json()); +app.use(cookieParser()); +app.use(express.urlencoded({ extended: false })); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', indexRouter); +app.use('/users', usersRouter); +app.use('/auth', authRouter); + +// catch 404 and forward to error handler +app.use(function (req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function (err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/1-Authentication/7-sign-in-express-mfa/App/auth/AuthProvider.js b/1-Authentication/7-sign-in-express-mfa/App/auth/AuthProvider.js new file mode 100644 index 000000000..e90fb6b1a --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/auth/AuthProvider.js @@ -0,0 +1,246 @@ +const msal = require('@azure/msal-node'); +const axios = require('axios'); +const { msalConfig, TENANT_SUBDOMAIN, REDIRECT_URI, POST_LOGOUT_REDIRECT_URI } = require('../authConfig'); + +class AuthProvider { + config; + cryptoProvider; + + constructor(config) { + this.config = config; + this.cryptoProvider = new msal.CryptoProvider(); + } + + getMsalInstance(msalConfig) { + return new msal.ConfidentialClientApplication(msalConfig); + } + + async login(req, res, next, options = {}) { + // create a GUID for crsf + req.session.csrfToken = this.cryptoProvider.createNewGuid(); + + /** + * The 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({ + csrfToken: req.session.csrfToken, + redirectTo: '/', + }) + ); + + 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 ?? [], + }; + + 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 ?? [], + }; + + /** + * 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. + */ + if (!this.config.msalConfig.auth.authorityMetadata) { + const authorityMetadata = await this.getAuthorityMetadata(); + this.config.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata); + } + + const msalInstance = this.getMsalInstance(this.config.msalConfig); + + // trigger the first leg of auth code flow + return this.redirectToAuthCodeUrl( + req, + res, + next, + authCodeUrlRequestParams, + authCodeRequestParams, + msalInstance + ); + } + + async handleRedirect(req, res, next) { + const authCodeRequest = { + ...req.session.authCodeRequest, + code: req.body.code, // authZ code + codeVerifier: req.session.pkceCodes.verifier, // PKCE Code Verifier + }; + + try { + const msalInstance = this.getMsalInstance(this.config.msalConfig); + msalInstance.getTokenCache().deserialize(req.session.tokenCache); + + const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body); + + req.session.tokenCache = msalInstance.getTokenCache().serialize(); + req.session.accessToken = tokenResponse.accessToken; + 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.redirectTo); + } catch (error) { + next(error); + } + } + + /** + * + * @param req: Express request object + * @param res: Express response object + * @param next: Express next function + * @param scopes: Array of strings + * @param redirectUri: redirect Url + */ + getToken(scopes, redirectUri = "http://localhost:3000/") { + return async function (req, res, next) { + console.log(scopes); + const msalInstance = authProvider.getMsalInstance(authProvider.config.msalConfig); + try { + msalInstance.getTokenCache().deserialize(req.session.tokenCache); + + const silentRequest = { + account: req.session.account, + scopes: scopes, + }; + + const tokenResponse = await msalInstance.acquireTokenSilent(silentRequest); + + req.session.tokenCache = msalInstance.getTokenCache().serialize(); + req.session.accessToken = tokenResponse.accessToken; + next(); + } catch (error) { + if (error instanceof msal.InteractionRequiredAuthError) { + req.session.csrfToken = authProvider.cryptoProvider.createNewGuid(); + + const state = authProvider.cryptoProvider.base64Encode( + JSON.stringify({ + redirectTo: 'http://localhost:3000/users/updateProfile', + csrfToken: req.session.csrfToken, + }) + ); + + const authCodeUrlRequestParams = { + state: state, + scopes: scopes, + }; + + const authCodeRequestParams = { + state: state, + scopes: scopes, + }; + + authProvider.redirectToAuthCodeUrl( + req, + res, + next, + authCodeUrlRequestParams, + authCodeRequestParams, + msalInstance + ); + } + + next(error); + } + }; + } + + async logout(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 + */ + const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`; + + req.session.destroy(() => { + res.redirect(logoutUri); + }); + } + + /** + * Prepares the auth code request parameters and initiates the first leg of auth code flow + * @param req: Express request object + * @param res: Express response object + * @param next: Express next function + * @param authCodeUrlRequestParams: parameters for requesting an auth code url + * @param authCodeRequestParams: parameters for requesting tokens using auth code + */ + async redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams, msalInstance) { + // 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, + redirectUri: this.config.redirectUri, + responseMode: 'form_post', // recommended for confidential clients + codeChallenge: req.session.pkceCodes.challenge, + codeChallengeMethod: req.session.pkceCodes.challengeMethod, + }; + + req.session.authCodeRequest = { + ...authCodeRequestParams, + redirectUri: this.config.redirectUri, + code: '', + }; + + try { + const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest); + res.redirect(authCodeUrlResponse); + } catch (error) { + next(error); + } + } + + /** + * Retrieves oidc metadata from the openid endpoint + * @returns + */ + async getAuthorityMetadata() { + const endpoint = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/v2.0/.well-known/openid-configuration`; + try { + const response = await axios.get(endpoint); + return await response.data; + } catch (error) { + console.log(error); + } + } +} + +const authProvider = new AuthProvider({ + msalConfig: msalConfig, + redirectUri: REDIRECT_URI, + postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI, +}); + +module.exports = authProvider; diff --git a/1-Authentication/7-sign-in-express-mfa/App/authConfig.js b/1-Authentication/7-sign-in-express-mfa/App/authConfig.js new file mode 100644 index 000000000..712297431 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/authConfig.js @@ -0,0 +1,49 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +require('dotenv').config({ path: '.env.dev' }); + +const TENANT_SUBDOMAIN = process.env.TENANT_SUBDOMAIN || 'Enter_the_Tenant_Subdomain_Here'; +const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/auth/redirect'; +const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI || 'http://localhost:3000'; + +/** + * 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 msalConfig = { + auth: { + clientId: process.env.CLIENT_ID || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Azure portal - this value is a GUID + authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // Replace the placeholder with your tenant name + clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app registration in Azure portal + }, + system: { + loggerOptions: { + loggerCallback(loglevel, message, containsPii) { + console.log(message); + }, + piiLoggingEnabled: false, + logLevel: 'Info', + }, + }, +}; + +const GRAPH_API_ENDPOINT = process.env.GRAPH_API_ENDPOINT || "graph_end_point"; +// Refers to the user that is single user singed in. +// https://learn.microsoft.com/en-us/graph/api/user-update?view=graph-rest-1.0&tabs=http +const GRAPH_ME_ENDPOINT = GRAPH_API_ENDPOINT + "v1.0/me"; + +const mfaProtectedResourceScope = process.env.MFA_PROTECTED_SCOPE || 'Add_your_protected_scope_here'; + +module.exports = { + msalConfig, + mfaProtectedResourceScope, + REDIRECT_URI, + POST_LOGOUT_REDIRECT_URI, + TENANT_SUBDOMAIN, + GRAPH_API_ENDPOINT, + GRAPH_ME_ENDPOINT, +}; diff --git a/1-Authentication/7-sign-in-express-mfa/App/controller/authController.js b/1-Authentication/7-sign-in-express-mfa/App/controller/authController.js new file mode 100644 index 000000000..d9af7cd6a --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/controller/authController.js @@ -0,0 +1,13 @@ +const authProvider = require('../auth/AuthProvider'); + +exports.signIn = async (req, res, next) => { + return authProvider.login(req, res, next, {scopes:["User.Read"]}); +}; + +exports.handleRedirect = async (req, res, next) => { + return authProvider.handleRedirect(req, res, next); +} + +exports.signOut = async (req, res, next) => { + return authProvider.logout(req, res, next); +}; diff --git a/1-Authentication/7-sign-in-express-mfa/App/fetch.js b/1-Authentication/7-sign-in-express-mfa/App/fetch.js new file mode 100644 index 000000000..c000fb8b5 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/fetch.js @@ -0,0 +1,33 @@ +var axios = require('axios'); +var authProvider = require("./auth/AuthProvider"); + +/** + * Makes an Authorization "Bearer" request with the given accessToken to the given endpoint. + * @param endpoint + * @param accessToken + * @param method + */ +const fetch = async (endpoint, accessToken, method = "GET", data = null) => { + const options = { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }; + console.log(`request made to ${endpoint} at: ` + new Date().toString()); + + switch (method) { + case 'GET': + const response = await axios.get(endpoint, options); + return await response.data; + case 'POST': + return await axios.post(endpoint, data, options); + case 'DELETE': + return await axios.delete(endpoint + `/${data}`, options); + case 'PATCH': + return await axios.patch(endpoint, ReqBody = data, options); + default: + return null; + } +}; + +module.exports = { fetch }; diff --git a/1-Authentication/7-sign-in-express-mfa/App/package-lock.json b/1-Authentication/7-sign-in-express-mfa/App/package-lock.json new file mode 100644 index 000000000..9dece07d7 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/package-lock.json @@ -0,0 +1,1141 @@ +{ + "name": "msal-node-auth-code", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "msal-node-auth-code", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^1.17.2", + "axios": "^1.0.0", + "cookie-parser": "^1.4.6", + "dotenv": "^16.0.3", + "express": "^4.18.1", + "express-session": "^1.17.3", + "hbs": "^4.2.0", + "http-errors": "^2.0.0", + "morgan": "^1.10.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz", + "integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.18.4.tgz", + "integrity": "sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==", + "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", + "dependencies": { + "@azure/msal-common": "13.3.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": "10 || 12 || 14 || 16 || 18" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==" + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/1-Authentication/7-sign-in-express-mfa/App/package.json b/1-Authentication/7-sign-in-express-mfa/App/package.json new file mode 100644 index 000000000..2c88dda12 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/package.json @@ -0,0 +1,21 @@ +{ + "name": "msal-node-auth-code", + "version": "1.0.0", + "description": "sample web app for msal-node", + "scripts": { + "start": "node server.js" + }, + "author": "Microsoft", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^1.17.2", + "axios": "^1.0.0", + "cookie-parser": "^1.4.6", + "dotenv": "^16.0.3", + "express": "^4.18.1", + "express-session": "^1.17.3", + "hbs": "^4.2.0", + "http-errors": "^2.0.0", + "morgan": "^1.10.0" + } +} diff --git a/1-Authentication/7-sign-in-express-mfa/App/public/stylesheets/style.css b/1-Authentication/7-sign-in-express-mfa/App/public/stylesheets/style.css new file mode 100644 index 000000000..9453385b9 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/1-Authentication/7-sign-in-express-mfa/App/routes/auth.js b/1-Authentication/7-sign-in-express-mfa/App/routes/auth.js new file mode 100644 index 000000000..0b2982817 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/routes/auth.js @@ -0,0 +1,9 @@ +const express = require('express'); +const authController = require('../controller/authController'); +const router = express.Router(); + +router.get('/signin', authController.signIn); +router.get('/signout', authController.signOut); +router.post('/redirect', authController.handleRedirect); + +module.exports = router; diff --git a/1-Authentication/7-sign-in-express-mfa/App/routes/index.js b/1-Authentication/7-sign-in-express-mfa/App/routes/index.js new file mode 100644 index 000000000..d0465d3e9 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/routes/index.js @@ -0,0 +1,17 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +const express = require('express'); +const router = express.Router(); + +router.get('/', function (req, res, next) { + res.render('index', { + title: 'MSAL Node & Express Web App', + isAuthenticated: req.session.isAuthenticated, + username: req.session.account?.username !== '' ? req.session.account?.username : req.session.account?.name, + }); +}); + +module.exports = router; diff --git a/1-Authentication/7-sign-in-express-mfa/App/routes/users.js b/1-Authentication/7-sign-in-express-mfa/App/routes/users.js new file mode 100644 index 000000000..c3572cea4 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/routes/users.js @@ -0,0 +1,80 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +const express = require('express'); +const router = express.Router(); +const authProvider = require('../auth/AuthProvider'); +var { fetch } = require("../fetch"); +const { GRAPH_ME_ENDPOINT, + mfaProtectedResourceScope } = require('../authConfig'); + +// custom middleware to check auth state +function isAuthenticated(req, res, next) { + if (!req.session.isAuthenticated) { + return res.redirect('/auth/signin'); // redirect to sign-in route + } + + next(); +}; + +router.get('/id', + isAuthenticated, // check if user is authenticated + async function (req, res, next) { + res.render('id', { idTokenClaims: req.session.account.idTokenClaims }); + } +); + +router.get( + '/updateProfile', + isAuthenticated, // check if user is authenticated + authProvider.getToken(["User.ReadWrite"]), // check for mfa + async function (req, res, next) { + const graphResponse = await fetch( + GRAPH_ME_ENDPOINT, + req.session.accessToken + ); + res.render("updateProfile", { + profile: graphResponse, + }); + } +); + +router.post( + '/update', + isAuthenticated, // check if user is authenticated + authProvider.getToken(["User.ReadWrite", mfaProtectedResourceScope]), // check for mfa + async function (req, res, next) { + try { + if (!!req.body) { + let body = req.body; + const graphEndpoint = GRAPH_ME_ENDPOINT; + // API that calls for a single singed in user. + // more infromation for this endpoint found here + // https://learn.microsoft.com/en-us/graph/api/user-update?view=graph-rest-1.0&tabs=http + fetch(graphEndpoint, req.session.accessToken, "PATCH", { + displayName: body.displayName, + givenName: body.givenName, + surname: body.surname, + mail: body.mail, + }) + .then((response) => { + if (response.status === 204) { + return res.redirect("/"); + } else { + next("Not updated"); + } + }) + .catch((error) => { + next(error); + }); + } else { + throw { error: "empty request" }; + } + } catch (error) { + next(error); + } + } +); +module.exports = router; diff --git a/1-Authentication/7-sign-in-express-mfa/App/server.js b/1-Authentication/7-sign-in-express-mfa/App/server.js new file mode 100644 index 000000000..9ca694e2f --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/server.js @@ -0,0 +1,90 @@ +/** + * Module dependencies. + */ + +var app = require('./app'); +var debug = require('debug')('msal:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + console.log(`Server listening on port ${port}`); + + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/1-Authentication/7-sign-in-express-mfa/App/views/error.hbs b/1-Authentication/7-sign-in-express-mfa/App/views/error.hbs new file mode 100644 index 000000000..065976562 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/views/error.hbs @@ -0,0 +1,3 @@ +

{{message}}

+

{{error.status}}

+
{{error.stack}}
diff --git a/1-Authentication/7-sign-in-express-mfa/App/views/id.hbs b/1-Authentication/7-sign-in-express-mfa/App/views/id.hbs new file mode 100644 index 000000000..92ddfc7f5 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/views/id.hbs @@ -0,0 +1,16 @@ +

Azure AD

+

ID Token

+ + + {{#each idTokenClaims}} + + + + + {{/each}} + +
{{@key}}{{this}}
+
+Learn about claims in this ID token +
+Go back diff --git a/1-Authentication/7-sign-in-express-mfa/App/views/index.hbs b/1-Authentication/7-sign-in-express-mfa/App/views/index.hbs new file mode 100644 index 000000000..ca87488a2 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/views/index.hbs @@ -0,0 +1,12 @@ +

{{title}}

+{{#if isAuthenticated }} +

Hi {{username}}!

+View ID token claims +
+Profile editing +
+Sign out +{{else}} +

Welcome to {{title}}

+Sign in +{{/if}} diff --git a/1-Authentication/7-sign-in-express-mfa/App/views/layout.hbs b/1-Authentication/7-sign-in-express-mfa/App/views/layout.hbs new file mode 100644 index 000000000..069e5b294 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/views/layout.hbs @@ -0,0 +1,13 @@ + + + + + {{title}} + + + + + {{{body}}} + + + diff --git a/1-Authentication/7-sign-in-express-mfa/App/views/updateProfile.hbs b/1-Authentication/7-sign-in-express-mfa/App/views/updateProfile.hbs new file mode 100644 index 000000000..a4991bb60 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/App/views/updateProfile.hbs @@ -0,0 +1,26 @@ +

Microsoft Graph API

+

/me endpoint response

+
+
+ + +
+ + +
+ + +
+ + + +
+ + + +
+ + +
+
+Go back diff --git a/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/AppCreationScripts.md b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/AppCreationScripts.md new file mode 100644 index 000000000..624c702ac --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/AppCreationScripts.md @@ -0,0 +1,138 @@ +# Registering sample apps with the Microsoft identity platform and updating configuration files using PowerShell + +## Overview + +### Quick summary + +1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. + + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 -TenantId "your test tenant's id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" + ``` + +### More details + +- [Goal of the provided scripts](#goal-of-the-provided-scripts) + - [Presentation of the scripts](#presentation-of-the-scripts) + - [Usage pattern for tests and DevOps scenarios](#usage-pattern-for-tests-and-DevOps-scenarios) +- [How to use the app creation scripts?](#how-to-use-the-app-creation-scripts) + - [Pre-requisites](#pre-requisites) + - [Run the script and start running](#run-the-script-and-start-running) + - [Four ways to run the script](#four-ways-to-run-the-script) + - [Option 1 (interactive)](#option-1-interactive) + - [Option 2 (Interactive, but create apps in a specified tenant)](#option-3-Interactive-but-create-apps-in-a-specified-tenant) + - [Running the script on Azure Sovereign clouds](#running-the-script-on-Azure-Sovereign-clouds) + +## Goal of the provided scripts + +### Presentation of the scripts + +This sample comes with two PowerShell scripts, which automate the creation of the Azure Active Directory applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test. + +These scripts are: + +- `Configure.ps1` which: + - creates Azure AD applications and their related objects (permissions, dependencies, secrets, app roles), + - changes the configuration files in the sample projects. + - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Azure AD application it created: + - the identifier of the application + - the AppId of the application + - the url of its registration in the [Azure portal](https://portal.azure.com). + +- `Cleanup.ps1` which cleans-up the Azure AD objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, `git reset`). + +> :information_source: If the sample supports using certificates instead of client secrets, this folder will contain an additional set of scripts: `Configure-WithCertificates.ps1` and `Cleanup-WithCertificates.ps1`. You can use them in the same way to register app(s) that use certificates instead of client secrets. + +### Usage pattern for tests and DevOps scenarios + +The `Configure.ps1` will stop if it tries to create an Azure AD application which already exists in the tenant. For this, if you are using the script to try/test the sample, or in DevOps scenarios, you might want to run `Cleanup.ps1` just before `Configure.ps1`. This is what is shown in the steps below. + +## How to use the app creation scripts? + +### Pre-requisites + +1. PowerShell 7 or later (see: [installing PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell)) +1. Open PowerShell (On Windows, press `Windows-R` and type `PowerShell` in the search window) + +### (Optionally) install Microsoft.Graph.Applications PowerShell modules + +The scripts install the required PowerShell module (Microsoft.Graph.Applications) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: + +1. If you have never done it already, in the PowerShell window, install the Microsoft.Graph.Applications PowerShell modules. For this: + + 1. Open PowerShell + 2. Type: + + ```PowerShell + Install-Module Microsoft.Graph.Applications + ``` + + or if you want the modules to be installed for the current user only, run: + + ```PowerShell + Install-Module Microsoft.Graph.Applications -Scope CurrentUser + ``` + +### Run the script and start running + +1. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo, + + ```PowerShell + cd AppCreationScripts + ``` + +1. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that. +1. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. +1. select **Start** for the projects + +You're done! + +### Two ways to run the script + +We advise four ways of running the script: + +- Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects, +- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, + +Here are the details on how to do this. + +#### Option 1 (interactive) + +- Just run ``.\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). +- The script will be run as the signed-in user and will use the tenant in which the user is defined. + +Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in. + +#### Option 2 (Interactive, but create apps in a specified tenant) + + if you want to create the apps in a particular tenant, you can use the following option: + +- Open the [Azure portal](https://portal.azure.com) +- Select the Azure Active directory you are interested in (in the combo-box below your name on the top right of the browser window) +- Find the "Active Directory" object in this tenant +- Go to **Properties** and copy the content of the **Directory Id** property +- Then use the full syntax to run the scripts: + +```PowerShell +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -TenantId $tenantId +. .\Configure.ps1 -TenantId $tenantId +``` + +### Running the script on Azure Sovereign clouds + +All the four options listed above can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. + +The acceptable values for this parameter are: + +- AzureCloud +- AzureChinaCloud +- AzureUSGovernment + +Example: + + ```PowerShell + . .\Cleanup.ps1 -AzureEnvironmentName "AzureUSGovernment" + . .\Configure.ps1 -AzureEnvironmentName "AzureUSGovernment" + ``` diff --git a/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Cleanup.ps1 b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Cleanup.ps1 new file mode 100644 index 000000000..90270d8a0 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Cleanup.ps1 @@ -0,0 +1,152 @@ +#Requires -Version 7 + +[CmdletBinding()] +param( + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] + [string] $azureEnvironmentName +) + + +Function Cleanup +{ + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "Global" + } + + <# + .Description + This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script + #> + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. + + # Connect to the Microsoft Graph API + Write-Host "Connecting to Microsoft Graph" + + + if ($tenantId -eq "") + { + Connect-MgGraph -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName + } + else + { + Connect-MgGraph -TenantId $tenantId -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName + } + + $context = Get-MgContext + $tenantId = $context.TenantId + + # Get the user running the script + $currentUserPrincipalName = $context.Account + $user = Get-MgUser -Filter "UserPrincipalName eq '$($context.Account)'" + + # get the tenant we signed in to + $Tenant = Get-MgOrganization + $tenantName = $Tenant.DisplayName + + $verifiedDomain = $Tenant.VerifiedDomains | where {$_.Isdefault -eq $true} + $verifiedDomainName = $verifiedDomain.Name + $tenantId = $Tenant.Id + + Write-Host ("Connected to Tenant {0} ({1}) as account '{2}'. Domain is '{3}'" -f $Tenant.DisplayName, $Tenant.Id, $currentUserPrincipalName, $verifiedDomainName) + + # Removes the applications + Write-Host "Cleaning-up applications from tenant '$tenantId'" + + Write-Host "Removing 'client' (ciam-msal-node-webapp) if needed" + try + { + Get-MgApplication -Filter "DisplayName eq 'ciam-msal-node-webapp'" | ForEach-Object {Remove-MgApplication -ApplicationId $_.Id } + } + catch + { + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to remove the application 'ciam-msal-node-webapp'. Error is $message. Try deleting manually." -ForegroundColor White -BackgroundColor Red + } + + Write-Host "Making sure there are no more (ciam-msal-node-webapp) applications found, will remove if needed..." + $apps = Get-MgApplication -Filter "DisplayName eq 'ciam-msal-node-webapp'" | Format-List Id, DisplayName, AppId, SignInAudience, PublisherDomain + + if ($apps) + { + Remove-MgApplication -ApplicationId $apps.Id + } + + foreach ($app in $apps) + { + Remove-MgApplication -ApplicationId $app.Id + Write-Host "Removed ciam-msal-node-webapp.." + } + + # also remove service principals of this app + try + { + Get-MgServicePrincipal -filter "DisplayName eq 'ciam-msal-node-webapp'" | ForEach-Object {Remove-MgServicePrincipal -ServicePrincipalId $_.Id -Confirm:$false} + } + catch + { + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to remove ServicePrincipal 'ciam-msal-node-webapp'. Error is $message. Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red + } +} + +# Pre-requisites +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph")) { + Install-Module "Microsoft.Graph" -Scope CurrentUser +} + +#Import-Module Microsoft.Graph + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) { + Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Authentication + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Identity.DirectoryManagement")) { + Install-Module "Microsoft.Graph.Identity.DirectoryManagement" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Identity.DirectoryManagement + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { + Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Applications + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Groups")) { + Install-Module "Microsoft.Graph.Groups" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Groups + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Users")) { + Install-Module "Microsoft.Graph.Users" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Users + +$ErrorActionPreference = "Stop" + + +try +{ + Cleanup -tenantId $tenantId -environment $azureEnvironmentName +} +catch +{ + $_.Exception.ToString() | out-host + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to register apps. Error is $message." -ForegroundColor White -BackgroundColor Red +} + +Write-Host "Disconnecting from tenant" +Disconnect-MgGraph diff --git a/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Configure.ps1 b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Configure.ps1 new file mode 100644 index 000000000..c7afff900 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/Configure.ps1 @@ -0,0 +1,352 @@ +#Requires -Version 7 + +[CmdletBinding()] +param( + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] + [string] $azureEnvironmentName +) + +<# + This script creates the Azure AD applications needed for this sample and updates the configuration files + for the visual Studio projects from the data in the Azure AD applications. + + In case you don't have Microsoft.Graph.Applications already installed, the script will automatically install it for the current user + + There are two ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. +#> + +# Create an application key +# See https://www.sabin.io/blog/adding-an-azure-active-directory-application-and-key-using-powershell/ +Function CreateAppKey([DateTime] $fromDate, [double] $durationInMonths) +{ + $key = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphPasswordCredential + + $key.StartDateTime = $fromDate + $key.EndDateTime = $fromDate.AddMonths($durationInMonths) + $key.KeyId = (New-Guid).ToString() + $key.DisplayName = "app secret" + + return $key +} + +# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure +# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is +# described in $permissionType +Function AddResourcePermission($requiredAccess, ` + $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) +{ + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) + { + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess += $resourceAccess + } + } + } +} + +# +# Example: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read" +# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell +Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal) +{ + # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique) + if ($servicePrincipal) + { + $sp = $servicePrincipal + } + else + { + $sp = Get-MgServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + } + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphResourceAccess] + + # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: + if ($requiredDelegatedPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2PermissionScopes -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + + # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application + if ($requiredApplicationPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} + + +<#.Description + This function takes a string input as a single line, matches a key value and replaces with the replacement value +#> +Function UpdateLine([string] $line, [string] $value) +{ + $index = $line.IndexOf(':') + $lineEnd = '' + + if($line[$line.Length - 1] -eq ','){ $lineEnd = ',' } + + if ($index -ige 0) + { + $line = $line.Substring(0, $index+1) + " " + '"' + $value+ '"' + $lineEnd + } + return $line +} + +<#.Description + This function takes a dictionary of keys to search and their replacements and replaces the placeholders in a text file +#> +Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = UpdateLine $line $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + +<#.Description + This function takes a string input as a single line, matches a key value and replaces with the replacement value +#> +Function ReplaceInLine([string] $line, [string] $key, [string] $value) +{ + $index = $line.IndexOf($key) + if ($index -ige 0) + { + $index2 = $index+$key.Length + $line = $line.Substring(0, $index) + $value + $line.Substring($index2) + } + return $line +} + +<#.Description + This function takes a dictionary of keys to search and their replacements and replaces the placeholders in a text file +#> +Function ReplaceInTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = ReplaceInLine $line $key $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + + +<#.Description + Primary entry method to create and configure app registrations +#> +Function ConfigureApplications +{ + <#.Description + This function creates the Azure AD applications for the sample in the provided Azure AD tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters + #> + + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "Global" + } + + # Connect to the Microsoft Graph API, non-interactive is not supported for the moment (Oct 2021) + Write-Host "Connecting to Microsoft Graph" + if ($tenantId -eq "") { + Connect-MgGraph -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName + } + else { + Connect-MgGraph -TenantId $tenantId -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName + } + + $context = Get-MgContext + $tenantId = $context.TenantId + + # Get the user running the script + $currentUserPrincipalName = $context.Account + $user = Get-MgUser -Filter "UserPrincipalName eq '$($context.Account)'" + + # get the tenant we signed in to + $Tenant = Get-MgOrganization + $tenantName = $Tenant.DisplayName + + $verifiedDomain = $Tenant.VerifiedDomains | where {$_.Isdefault -eq $true} + $verifiedDomainName = $verifiedDomain.Name + $tenantId = $Tenant.Id + + Write-Host ("Connected to Tenant {0} ({1}) as account '{2}'. Domain is '{3}'" -f $Tenant.DisplayName, $Tenant.Id, $currentUserPrincipalName, $verifiedDomainName) + + # Create the client AAD application + Write-Host "Creating the AAD application (ciam-msal-node-webapp)" + # Get a 6 months application key for the client Application + $fromDate = [DateTime]::Now; + $key = CreateAppKey -fromDate $fromDate -durationInMonths 6 + + # create the application + $clientAadApplication = New-MgApplication -DisplayName "ciam-msal-node-webapp" ` + -Web ` + @{ ` + RedirectUris = "http://localhost:3000", "http://localhost:3000/auth/redirect"; ` + HomePageUrl = "http://localhost:3000"; ` + } ` + -SignInAudience AzureADMyOrg ` + #end of command + + #add a secret to the application + $pwdCredential = Add-MgApplicationPassword -ApplicationId $clientAadApplication.Id -PasswordCredential $key + $clientAppKey = $pwdCredential.SecretText + + $currentAppId = $clientAadApplication.AppId + $currentAppObjectId = $clientAadApplication.Id + + $tenantName = (Get-MgApplication -ApplicationId $currentAppObjectId).PublisherDomain + #Update-MgApplication -ApplicationId $currentAppObjectId -IdentifierUris @("https://$tenantName/ciam-msal-node-webapp") + + # create the service principal of the newly created application + $clientServicePrincipal = New-MgServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-MgApplicationOwner -ApplicationId $currentAppObjectId + if ($owner -eq $null) + { + New-MgApplicationOwnerByRef -ApplicationId $currentAppObjectId -BodyParameter @{"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$user.ObjectId"} + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + Write-Host "Done creating the client application (ciam-msal-node-webapp)" + + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$currentAppId+"/objectId/"+$currentAppObjectId+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/"+$currentAppId+"/isMSAApp~/false" + + Add-Content -Value "client$currentAppIdciam-msal-node-webapp" -Path createdApps.html + # Declare a list to hold RRA items + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermission = GetRequiredPermissions -applicationDisplayName "Microsoft Graph"` + -requiredDelegatedPermissions "openid|offline_access" + + $requiredResourcesAccess.Add($requiredPermission) + Write-Host "Added 'Microsoft Graph' to the RRA list." + # Useful for RRA additions troubleshooting + # $requiredResourcesAccess.Count + # $requiredResourcesAccess + + Update-MgApplication -ApplicationId $currentAppObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + + # print the registered app portal URL for any further navigation + Write-Host "Successfully registered and configured that app registration for 'ciam-msal-node-webapp' at `n $clientPortalUrl" -ForegroundColor Green + + # Update config file for 'client' + # $configFile = $pwd.Path + "\..\App\authConfig.js" + $configFile = $(Resolve-Path ($pwd.Path + "\..\App\authConfig.js")) + + $dictionary = @{ "Enter_the_Application_Id_Here" = $clientAadApplication.AppId; "Enter_the_Tenant_Subdomain_Here" = $tenantName.Split(".onmicrosoft.com")[0]; "Enter_the_Client_Secret_Here" = $clientAppKey }; + + Write-Host "Updating the sample config '$configFile' with the following config values:" -ForegroundColor Yellow + $dictionary + Write-Host "-----------------" + + ReplaceInTextFile -configFilePath $configFile -dictionary $dictionary + Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" + Write-Host "IMPORTANT: Please follow the instructions below to complete a few manual step(s) in the Azure portal": + Write-Host "- For client" + Write-Host " - Navigate to $clientPortalUrl" + Write-Host " - Navigate to your tenant and create user flows to allow users to sign up for the application." -ForegroundColor Red + Write-Host " - The delegated permissions for the 'client' application require admin consent. Do remember to navigate to the application registration in the app portal and consent for those." -ForegroundColor Red + Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" + +Add-Content -Value "" -Path createdApps.html +} # end of ConfigureApplications function + +# Pre-requisites + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph")) { + Install-Module "Microsoft.Graph" -Scope CurrentUser +} + +#Import-Module Microsoft.Graph + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) { + Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Authentication + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Identity.DirectoryManagement")) { + Install-Module "Microsoft.Graph.Identity.DirectoryManagement" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Identity.DirectoryManagement + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { + Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Applications + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Groups")) { + Install-Module "Microsoft.Graph.Groups" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Groups + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Users")) { + Install-Module "Microsoft.Graph.Users" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Users + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +$ErrorActionPreference = "Stop" + +# Run interactively (will ask you for the tenant ID) + +try +{ + ConfigureApplications -tenantId $tenantId -environment $azureEnvironmentName +} +catch +{ + $_.Exception.ToString() | out-host + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to register apps. Error is $message." -ForegroundColor White -BackgroundColor Red +} +Write-Host "Disconnecting from tenant" +Disconnect-MgGraph \ No newline at end of file diff --git a/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/apps.json b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/apps.json new file mode 100644 index 000000000..a60bfc9f3 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/apps.json @@ -0,0 +1,60 @@ +{ + "Sample": { + "Title": "A JavaScript single-page application using MSAL Angular to authenticate users against Azure AD for Customers", + "Client": "Node (Express) web app", + "Level": 100 + }, + "AppRegistrations": [ + { + "x-ms-id": "ciam-express-webapp", + "x-ms-name": "ms-identity-ciam-express-webapp", + "x-ms-version": "2.0", + "replyUrlsWithType": [ + { + "url": "http://localhost:3000/", + "type": "Web" + }, + { + "url": "http://localhost:3000/auth/redirect", + "type": "Web" + } + ], + "oauth2AllowImplicitFlow": false, + "oauth2AllowIdTokenImplicitFlow": false, + "x-ms-passwordCredentials": "Auto", + "passwordCredentials": [ + { + "value": "{auto}" + } + ], + "requiredResourceAccess": [ + { + "x-ms-resourceAppName": "Microsoft Graph", + "resourceAppId": "00000003-0000-0000-c000-000000000000", + "resourceAccess": [ + { + "id": "37f7f235-527c-4136-accd-4a02d197296e", + "type": "Scope", + "x-ms-name": "openid" + }, + { + "id": "7427e0e9-2fba-42fe-b0c0-848c9e6a8182", + "type": "Scope", + "x-ms-name": "offline_access" + } + ] + } + ], + "codeConfigurations": [ + { + "settingFile": "App/authConfig.js", + "replaceTokens": { + "appId": "Enter_the_Application_Id_Here", + "tenantName": "Enter_the_Tenant_Subdomain_Here", + "clientSecret": "Enter_the_Client_Secret_Here" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/quickstart.md b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/quickstart.md new file mode 100644 index 000000000..32450451e --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/quickstart.md @@ -0,0 +1,30 @@ +--- +title: "Portal quickstart for Node (Express) web app" +description: Learn how to run a sample Node (Express) web application to sign in users +services: active-directory +author: kengaderdus +manager: mwongerapk +ms.author: kengaderdus +ms.service: active-directory +ms.workload: identity +ROBOTS: NOINDEX +ms.subservice: ciam +ms.topic: portal +ms.date: 04/19/2023 +--- +# Portal quickstart for Node (Express) web app + +> [!div renderon="portal" id="display-on-portal" class="sxs-lookup"] +> In this quickstart, you download and run a code sample that demonstrates how a Node (Express) web application that can sign in users with Azure AD for Customers. +> +> 1. Make sure you've installed [Node.js](https://nodejs.org/download/). +> 1. Unzip the sample. +> 1. Locate the sample folder in your terminal, then run the following commands: +> +> ```console +> cd App && npm install && npm start +> ``` +> +> 1. Visit `http://localhost:3000` in your browser. +> 1. Select **Sign-in** on the navigation bar, then follow the prompts. +> \ No newline at end of file diff --git a/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/sample.json b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/sample.json new file mode 100644 index 000000000..b73ab7c73 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/AppCreationScripts/sample.json @@ -0,0 +1,67 @@ +{ + "Sample": { + "Title": "A Node.js & Express web app authenticating users against Azure AD for Customers with MSAL Node", + "Level": 100, + "Client": "Node.js & Express web app", + "Languages": [ + "javascript" + ], + "Products": [ + "azure-active-directory", + "msal-node" + ], + "RepositoryUrl": "ms-identity-ciam-javascript-tutorial", + "Endpoint": "AAD v2.0", + "Provider": "CIAM", + "Platform": "JavaScript", + "description": "This sample demonstrates a Node.js & Express web app authenticating users against Azure Active Directory Customer Identity Access Management (Azure AD for Customers) with Microsoft Authentication Library for Node (MSAL Node)" + }, + "AADApps": [ + { + "Id": "client", + "Name": "ciam-msal-node-webapp", + "Kind": "WebApp", + "Audience": "AzureADMyOrg", + "HomePage": "http://localhost:3000", + "ReplyUrls": "http://localhost:3000, http://localhost:3000/auth/redirect", + "PasswordCredentials": "Auto", + "SDK": "MsalNode", + "SampleSubPath": "1-Authentication\\5-sign-in-express\\App", + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": [ + "openid", + "offline_access" + ] + } + ], + "ManualSteps": [ + { + "Comment": "Navigate to your tenant and create user flows to allow users to sign up for the application." + } + ] + } + ], + "CodeConfiguration": [ + { + "App": "client", + "SettingKind": "Replace", + "SettingFile": "\\..\\App\\authConfig.js", + "Mappings": [ + { + "key": "Enter_the_Application_Id_Here", + "value": ".AppId" + }, + { + "key": "Enter_the_Tenant_Subdomain_Here", + "value": "$tenantName" + }, + { + "key": "Enter_the_Client_Secret_Here", + "value": ".AppKey" + } + ] + } + ] +} \ No newline at end of file diff --git a/1-Authentication/7-sign-in-express-mfa/README-use-certificate.md b/1-Authentication/7-sign-in-express-mfa/README-use-certificate.md new file mode 100644 index 000000000..d44415902 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/README-use-certificate.md @@ -0,0 +1,311 @@ + +# How to use certificates instead of secrets in your application(s) + +Microsoft identity platform supports two types of authentication for [confidential client applications](https://learn.microsoft.com/azure/active-directory/develop/msal-client-applications): password-based authentication (i.e. client secret) and certificate-based authentication. For a higher level of security, we recommend using a certificate (instead of a client secret) as a credential in your confidential client applications. + +In production, you should purchase a certificate signed by a well-known certificate authority, and use [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) to manage certificate access and lifetime for you. For testing purposes, follow the steps below to create a self-signed certificate and configure your apps to authenticate with certificates. + +## Using certificates + +
+:information_source: Expand this to use automation + +> :warning: Make sure you have OpenSSL installed on your machine. After installation, you may need to start a new command line instance for the `openssl` command to be available on system path. +> +> ```console +> choco install openssl +> ``` + +Alternatively, download and build **OpenSSL** for your **OS** following the guide at [github.com/openssl](https://github.com/openssl/openssl#build-and-install). If you like to skip building and get a binary distributable from the community instead, check the [OpenSSL Wiki: Binaries](https://wiki.openssl.org/index.php/Binaries) page. + + +1. While inside *AppCreationScripts* folder, open a terminal. + +2. Run the [Cleanup-withCertCertificates.ps1](./Cleanup-withCertCertificates.ps1) script to delete any existing app registrations and certificates for the sample. + +```console + .\Cleanup-withCertCertificates.ps1 +``` + +3. Run the [Configure-withCertCertificates.ps1](./Configure-withCertCertificates.ps1) script to re-create the App Registration. The script will also create `.pfx` file(s) (e.g. ciam-msal-node-webapp.pfx) that you can upload to Key Vault later. When asked about a password, do remember it - you will need the password when uploading the certificate. + +```console + .\Configure-withCertCertificates.ps1 +``` + +4. Proceed to [step 3](#configure-your-apps-to-use-a-certificate) to configure application settings. + +
+ +- **Step 1: [Create a self-signed certificate](#create-a-self-signed-certificate)** + - Option 1: [create self-signed certificate on local machine](#create-self-signed-certificate-on-local-machine) + - Option 2: [create self-signed certificate on Key Vault](#create-self-signed-certificate-on-key-vault) +- **Step 2: [Configure an Azure AD app registration to use a certificate](#configure-an-azure-ad-app-registration-to-use-a-certificate)** +- **Step 3: [Configure your app(s) to use a certificate](#configure-your-apps-to-use-a-certificate)** + - Option 1: [using an existing certificate from local machine](#using-an-existing-certificate-from-local-machine) + - Option 2: [using an existing certificate from Key Vault](#using-an-existing-certificate-from-key-vault) + +If you plan to deploy your app(s) to [Azure App Service](https://learn.microsoft.com/azure/app-service/overview) afterwards, we recommend [Azure Managed Identity](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) to completely eliminate secrets, certificates, connection strings and etc. from your source code. See [Using Managed Identity](#using-managed-identity) below for more. + +### Create a self-signed certificate + +You can skip this step if you already have a valid self-signed certificate at hand. + +#### Create self-signed certificate on local machine + +If you wish to generate a new self-signed certificate yourself, follow the steps below. +
+Click here to use OpenSSL + +Download and build **OpenSSL** for your **OS** following the guide at [github.com/openssl](https://github.com/openssl/openssl#build-and-install). If you like to skip building and get a binary distributable from the community instead, check the [OpenSSL Wiki: Binaries](https://wiki.openssl.org/index.php/Binaries) page. Afterwards, add the path to **OpenSSL** to your **environment variables** so that you can call it from anywhere. + +Type the following in a terminal. The files will be generated in the terminals current directory. + +```bash +openssl req -x509 -newkey rsa:2048 -keyout ciam-msal-node-webapp.key -out ciam-msal-node-webapp.cer -subj "/CN=ciam-msal-node-webapp" -nodes + +Generating a RSA private key +......................................................... +writing new private key to 'ciam-msal-node-webapp.key' +``` + +The following files should be generated: *ciam-msal-node-webapp.key*, *ciam-msal-node-webapp.cer* + +If you need, you can generate a ciam-msal-node-webapp.pfx (certificate + private key combination) with the command below: + +```bash +openssl pkcs12 -export -out CertificateName.pfx -inkey ciam-msal-node-webapp.key -in ciam-msal-node-webapp.cer +``` + +Enter an export password when prompted and make a note of it. The following file should be generated: *ciam-msal-node-webapp.pfx*. + +Proceed to [Step 2](#configure-an-azure-ad-app-registration-to-use-a-certificate). + +
+ +> :information_source: If you wish so, you can upload your locally generated self-signed certificate to Azure Key Vault later on. See: [Import a certificate in Azure Key Vault](https://learn.microsoft.com/azure/key-vault/certificates/tutorial-import-certificate) + +#### Create self-signed certificate on Key Vault + +You can use Azure Key Vault to generate a self-signed certificate for you. Doing so will have the additional benefits of assigning a partner Certificate Authority (CA) and automating certificate rotation. + +> :information_source: Azure Key Vault can export certificates and private keys in `pem` format (see: [Export stored certificates](https://docs.microsoft.com/azure/key-vault/certificates/how-to-export-certificate?tabs=azure-cli#export-stored-certificates)), if **Content Type** was chosen as `pem` during certificate generation (see: [Create a certificate in Key Vault](https://docs.microsoft.com/azure/key-vault/certificates/tutorial-rotate-certificates#create-a-certificate-in-key-vault)). If for some reason this is not the case, OpenSSL can be used for conversions. +> +> ```console +> cat ciam-msal-node-webapp.crt ciam-msal-node-webapp.key > ciam-msal-node-webapp.pem ## if powershell: Get-Content ciam-msal-node-webapp.crt, ciam-msal-node-webapp.key | Set-Content ciam-msal-node-webapp.pem +> openssl pkcs12 -in ciam-msal-node-webapp.pfx -out ciam-msal-node-webapp.pem +> ``` + +
+Click here to use Azure Portal + +Follow the guide: [Set and retrieve a certificate from Azure Key Vault using the Azure portal](https://learn.microsoft.com/azure/key-vault/certificates/quick-create-portal) + +Afterwards, proceed to [Step 2](#configure-an-azure-ad-app-registration-to-use-a-certificate). + +
+ +
+Click here to use Powershell + +Follow the guide: [Set and retrieve a certificate from Azure Key Vault using Azure PowerShell](https://learn.microsoft.com/azure/key-vault/certificates/quick-create-powershell) + +Afterwards, proceed to [Step 2](#configure-an-azure-ad-app-registration-to-use-a-certificate). + +
+ +### Configure an Azure AD app registration to use a certificate + +Now you must associate your Azure AD app registration with the certificate you will use in your application. + +> :information_source: If you have the certificate locally available, you can follow the steps below. If your certificate(s) is on Azure Key Vault, you must first export and download them to your computer, and delete the local copy after following the steps below. See: [Export certificates from Azure Key Vault](https://learn.microsoft.com/azure/key-vault/certificates/how-to-export-certificate) + +1. Navigate to [Azure portal](https://portal.azure.com) and select your Azure AD app registration. +1. Select **Certificates & secrets** blade on the left. +1. Click on **Upload** certificate and select the certificate file to upload (e.g. *ciam-msal-node-webapp*). +1. Click **Add**. Once the certificate is uploaded, the *thumbprint*, *start date*, and *expiration* values are displayed. Record the *thumbprint* value as you will make use of it later in your app's configuration file. + +> For more information, see: [Register your certificate with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-microsoft-identity-platform) + +Proceed to [Step 3](#configure-your-apps-to-use-a-certificate) + +### Configure your app(s) to use a certificate + +Finally, you need to modify the app's configuration files. + +#### Using an existing certificate from local machine + +> Perform the steps below for the client app (ciam-msal-node-webapp) + +1. Open the file where MSAL is initialized (e.g. `1-Authentication\5-sign-in-express\App\app.js`). +2. *Comment out* the line for `clientSecret`: + +```javaScript + const msal = require('@azure/msal-node'); + + const msalConfig = { + auth: { + clientId: "YOUR_CLIENT_ID", + authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", + //clientSecret: "YOUR_CLIENT_SECRET" + } + }; + + const cca = new msal.ConfidentialClientApplication(msalConfig); +``` + +3. *Un-comment* the lines for `clientCertificate` and replace the default values: + +```javaScript + const msal = require('@azure/msal-node'); + const fs = require('fs'); // import the fs module for reading the key file + + const msalConfig = { + auth: { + clientId: "YOUR_CLIENT_ID", + authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", + //clientSecret: "YOUR_CLIENT_SECRET" + clientCertificate: { + thumbprint: "YOUR_CERT_THUMBPRINT", // replace with thumbprint obtained during step 2 above + privateKey: fs.readFileSync('PATH_TO_YOUR_PRIVATE_KEY_FILE'), // e.g. c:/Users/diego/Desktop/example.key + } + } + }; + + const cca = new msal.ConfidentialClientApplication(msalConfig); +``` + +> :information_source: For more details, see: [initializing-msal-node-with-certificates](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/certificate-credentials.md#initializing-msal-node-with-certificates) + +You can now start the application as instructed in the [README](./README#setup-the-sample). + +#### Using an existing certificate from Key Vault + +> Perform the steps below for the client app (ciam-msal-node-webapp) + +1. Open the file where MSAL is initialized (e.g. `1-Authentication\5-sign-in-express\App\app.js`). +2. *Comment out* the line for `clientSecret`: + +```javaScript + const msal = require('@azure/msal-node'); + + const msalConfig = { + auth: { + clientId: "YOUR_CLIENT_ID", + authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", + //clientSecret: "YOUR_CLIENT_SECRET" + } + }; + + const cca = new msal.ConfidentialClientApplication(msalConfig); +``` + +3. Install **[Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli)**. Then, type the following to sign-in: + +```console + az login --tenant YOUR_TENANT_ID +``` + +4. Install the required NPM packages: + +```console + npm install --save @azure/identity @azure/keyvault-certificates @azure/keyvault-secrets +``` + +5. Update the code as shown below: + +```javaScript + const msal = require('@azure/msal-node'); + const identity = require("@azure/identity"); + const keyvaultCert = require("@azure/keyvault-certificates"); + const keyvaultSecret = require('@azure/keyvault-secrets'); + + const KV_URL = process.env["KEY_VAULT_URL"] || "ENTER_YOUR_KEY_VAULT_URL" + const CERTIFICATE_NAME = process.env["CERTIFICATE_NAME"] || "ENTER_THE_NAME_OF_YOUR_CERTIFICATE_ON_KEY_VAULT"; + + // Initialize Azure SDKs + const credential = new identity.DefaultAzureCredential(); + const certClient = new keyvaultCert.CertificateClient(KV_URL, credential); + const secretClient = new keyvaultSecret.SecretClient(KV_URL, credential); + + async function main() { + + // Grab the certificate thumbprint + const certResponse = await certClient.getCertificate(CERTIFICATE_NAME).catch(err => console.log(err)); + const thumbprint = certResponse.properties.x509Thumbprint.toString('hex') + + // When you upload a certificate to Key Vault, a secret containing your private key is automatically created + const secretResponse = await secretClient.getSecret(CERTIFICATE_NAME).catch(err => console.log(err));; + + // secretResponse contains both public and private key, but we only need the private key + const privateKey = secretResponse.value.split('-----BEGIN CERTIFICATE-----\n')[0] + + const msalConfig = { + auth: { + clientId: "YOUR_CLIENT_ID", + authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", + //clientSecret: "YOUR_CLIENT_SECRET", + clientCertificate: { + thumbprint: thumbprint + privateKey: privateKey + } + } + }; + + const cca = new msal.ConfidentialClientApplication(msalConfig); + } + + main(); +``` + +> :information_source: For more details, see: [Get certificate from your Key Vault in Node.js](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/key-vault-managed-identity.md#get-certificate-from-your-vault-in-nodejs) + +You can now start the application as instructed in the [README](./README#setup-the-sample). + +## Using Managed Identity + +Once you deploy your app(s) to Azure App Service, you can assign a managed identity to it for accessing Azure Key Vault using its own identity. This allows you to eliminate the all secrets, certificates, connection strings and etc. from your source code. + +### Create a system-assigned identity + +1. Navigate to [Azure portal](https://portal.azure.com) and select the **Azure App Service**. +1. Find and select the App Service instance you've created previously. +1. On App Service portal, select **Identity**. +1. Within the **System assigned** tab, switch **Status** to **On**. Click **Save**. + +For more information, see [Add a system-assigned identity](https://docs.microsoft.com/azure/app-service/overview-managed-identity?tabs=dotnet#add-a-system-assigned-identity) + +### Grant access to Key Vault + +Now that your app deployed to App Service has a managed identity, in this step you grant it access to your key vault. + +1. Go to the [Azure portal](https://portal.azure.com) and search for your Key Vault. +1. Select **Overview** > **Access policies** blade on the left. +1. Click on **Add Access Policy** > **Certificate permissions** > **Get** +1. Click on **Add Access Policy** > **Secret permissions** > **Get** +1. Click on **Select Principal**, add your account and pre-created **system-assigned** identity. +1. Click on **OK** to add the new Access Policy, then click **Save** to save the Access Policy. + +For more information, see [Use Key Vault from App Service with Azure Managed Identity](https://docs.microsoft.com/samples/azure-samples/app-service-msi-keyvault-dotnet/keyvault-msi-appservice-sample/) + +### Add environment variables + +Finally, you need to add environment variables to the App Service where you deployed your app. + +> :warning: Make sure your application is able to read environment variables. Alternatively, you can hardcode the key vault URL and certificate name in your applications configuration file. + +1. In the [Azure portal](https://portal.azure.com), search for and select **App Service**, and then select your app. +1. Select **Configuration** blade on the left, then select **New Application Settings**. +1. Add the following variables (key-value pairs): + 1. **KEY_VAULT_URL**: the URL of the key vault you've created, e.g. `https://example.vault.azure.net` + 1. **CERTIFICATE_NAME**: the name of the certificate you specified when importing it to key vault, e.g. `ExampleCert` + +Wait for a few minutes for your changes on **App Service** to take effect. You should then be able to visit your published website and sign-in accordingly. + +## More information + +- [Microsoft identity platform application authentication certificate credentials](https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials) +- [Create a self-signed public certificate to authenticate your application](https://docs.microsoft.com/azure/active-directory/develop/howto-create-self-signed-certificate) +- [Various SSL/TLS certificate file types/extensions](https://docs.microsoft.com/archive/blogs/kaushal/various-ssltls-certificate-file-typesextensions) +- [Azure Key Vault Developer's Guide](https://docs.microsoft.com/azure/key-vault/general/developers-guide) +- [Managed identities for Azure resources](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) diff --git a/1-Authentication/7-sign-in-express-mfa/README.md b/1-Authentication/7-sign-in-express-mfa/README.md new file mode 100644 index 000000000..106d13948 --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/README.md @@ -0,0 +1,371 @@ +--- +page_type: sample +name: Sign in users in a sample Node.js & have a mfa on editing profile information & Express web app by using Microsoft Entra External ID for customers +description: This sample demonstrates a Node.js with editing profile gated behind a mfa & Express web app authenticating users by using Microsoft Entra External ID for customers with Microsoft Authentication Library for Node (MSAL Node) +languages: + - javascript +products: + - entra-external-id + - msal-node +urlFragment: ms-identity-ciam-javascript-tutorial-5-sign-in-express-mfa +extensions: + services: + - active-directory + sub-service: + - ciam + platform: + - JavaScript + endpoint: + - AAD v2.0 + level: + - 100 + client: + - Node.js & Express web app +--- + +# Sign in users in a sample Node.js (Express.js) web app by using Microsoft Entra External ID for customers + +* [Overview](#overview) +* [Usage](#usage) +* [Scenario](#scenario) +* [Contents](#contents) +* [Prerequisites](#prerequisites) +* [Setup the sample](#setup-the-sample) +* [Explore the sample](#explore-the-sample) +* [Troubleshooting](#troubleshooting) +* [About the code](#about-the-code) +* [Contributing](#contributing) +* [Learn More](#learn-more) + +## Overview + +This sample demonstrates how to sign users and edit profile which requires mfa into a sample Node.js & Express web app by using Microsoft Entra External ID for customers. The samples utilizes the [Microsoft Authentication Library for Node](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node) (MSAL Node) to simplify adding authentication to the Node.js web app. + +## Usage + +| Instruction | Description | +|-----------------------|--------------------------------------------| +| **Use case** | This code sample applies to **customer configuration uses case**![Yes button](./ReadmeFiles/yes.png "Title"). If you're looking for a workforce configuration use case, use [Tutorial: Enable a Node.js (Express) application to sign in users by using Microsoft Entra ID](https://github.com/Azure-Samples/ms-identity-node) | +| **Scenario** | Sign in users. You acquire an ID token by using authorization code flow with PKCE. Edit user profile which requires mfa | +| **Add sign in to your app** | Use the instructions in [Sign in users in a Node.js web app](https://learn.microsoft.com/entra/external-id/customers/tutorial-web-app-node-sign-in-prepare-tenant) to learn how to add sign in to your Node web app. | +|**Product documentation** | Explore [Microsoft Entra ID for customers documentation](https://learn.microsoft.com/entra/external-id/customers/) | + +## Contents + +| File/folder | Description | +|-----------------------|--------------------------------------------| +| `App/app.js` | Application entry point. | +| `App/authConfig.js` | Contains authentication parameters such as your tenant sub-domain, Application (Client) ID, app client secret and redirect URI. | +| `App/auth/AuthProvider.js` | The main authentication logic resides here. | +| `/App/views/` | This folder contains app views. This Node/Express sample app's views uses Handlebars. | +| `/App/routes/` | This folder contains app's routes. | + +## Prerequisites + +* You must install [Node.js](https://nodejs.org/en/download/) in your computer to run this sample. +* We recommend [Visual Studio Code](https://code.visualstudio.com/download) for running and editing this sample. +* Microsoft Entra ID for customers tenant. If you don't already have one, [sign up for a free trial](https://aka.ms/ciam-free-trial). +* If you'd like to use Azure services, such as hosting your app in Azure App Service, [VS Code Azure Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack) extension is recommended for interacting with Azure through VS Code Interface. + +## Register the web application in your tenant + +You can register an app in your tenant automatically by using Microsoft Graph PowerShell or via the Microsoft Entra Admin center. + +When you use Microsoft Graph PowerShell, you automatically register the applications and related objects app secrets, then modify your project config files, so you can run the app without any further action: + + +* To register your app in the Microsoft Entra admin center use the steps in [Register the web app](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#register-the-web-app). + +* To register and configure your app automatically, + +
+ Expand this section + + > :warning: If you have never used **Microsoft Graph PowerShell** before, we recommend you go through the [App Creation Scripts Guide](./AppCreationScripts/AppCreationScripts.md) once to ensure that you've prepared your environment correctly for this step. + + 1. Ensure that you have PowerShell 7 or later installed. + 1. Run the script to create your Microsoft Entra ID application and configure the code of the sample application accordingly. + 1. For interactive process in PowerShell, run: + + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" + ``` + + > Other ways of running the scripts are described in [App Creation Scripts guide](./AppCreationScripts/AppCreationScripts.md). The scripts also provides a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + + > :exclamation: NOTE: This sample can make use of client certificates. You can use **AppCreationScripts** to register an Microsoft Entra ID application with certificates. For more information see, [Use client certificate for authentication in your Node.js web app instead of client secrets](https://learn.microsoft.com/entra/external-id/customers/how-to-web-app-node-use-certificate). + +
+ +## Add app client secret + +To create a client secret for the registered application, use the steps in [Add app client secret](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#add-app-client-secret) + +## Grant API permissions + +To grant delegated permissions, use the steps in [Grant API permissions](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#grant-api-permissions). + +## Create user flow + +To create a user flow a customer can use to sign in or sign up for an application, use the steps in [Create a user flow](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#create-a-user-flow) + +## Associate the web application with the user flow + +To associate the web application with the user flow, use the steps in [Associate the web application with the user flow](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#associate-the-web-application-with-the-user-flow). + +## Clone or download sample web application + +To get the web app sample code, use the steps in [Clone or download sample web application](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#clone-or-download-sample-web-application). + +## Install project dependencies + +To install app dependencies, use the steps in [Install project dependencies](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#install-project-dependencies). + +## Configure the sample web app to use your app registration + +Once you download the sample app, you need to update it so that it uses the settings of the web app that you registered. To do so, use the steps in [Configure the sample web app](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#configure-the-sample-web-app). + +## Run and test sample web app + +You can now test the sample Node.js web app. You need to start the Node.js server and access it through your browser at `http://localhost:3000`. To do so, use the steps in [Run and test sample web app](https://learn.microsoft.com/entra/external-id/customers/sample-web-app-node-sign-in#run-and-test-sample-web-app). + +> :information_source: If the sample didn't work for you as expected, reach out to us using the [GitHub Issues](../../../../issues) page. + +## We'd love your feedback + +Were we successful in addressing your learning objective? Consider taking a moment to [share your experience with us](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR_ivMYEeUKlEq8CxnMPgdNZUNDlUTTk2NVNYQkZSSjdaTk5KT1o4V1VVNS4u). + +## Troubleshooting + +
+ Expand for troubleshooting info + +> * Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. Make sure that your questions or comments are tagged with [`azure-active-directory-b2c` `node` `ms-identity` `adal` `msal-js` `msal`]. + +To provide feedback on or suggest features for Microsoft Entra ID or Microsoft Entra External ID, visit [User Voice page](https://feedback.azure.com/d365community/forum/79b1327d-d925-ec11-b6e6-000d3a4f06a4). +
+ +## About the code + +### Initialization + +In order to use MSAL Node, we instantiate the [ConfidentialClientApplication](https://learn.microsoft.com/javascript/api/@azure/msal-node/confidentialclientapplication?view=azure-node-latest): + +1. Create the configuration object, `msalConfig`, as shown in the *App/authConfig.js* file: + + ```javascript + const msalConfig = { + auth: { + clientId: process.env.CLIENT_ID || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Microsoft Entra - this value is a GUID + authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // Replace the placeholder with your tenant name + clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app registration in Azure portal + }, + ... + ... + }; + ``` + +1. Use the `msalConfig` object to instantiate the confidential client application shown in the *App/auth/AuthProvider.js file (`AuthProvider` class): + + ```javascript + ... + ... + getMsalInstance(msalConfig) { + return new msal.ConfidentialClientApplication(msalConfig); + } + .... + ... + ``` + +### Sign in + +The first leg of auth code flow generates an authorization code request URL, then redirects to that URL to obtain the authorization code. This first leg is implemented in the `redirectToAuthCodeUrl` method. Notice how we use MSALs [getAuthCodeUrl](https://learn.microsoft.com/javascript/api/%40azure/msal-node/confidentialclientapplication?view=azure-node-latest#@azure-msal-node-confidentialclientapplication-getauthcodeurl) method to generate authorization code URL, then redirect to the authorization code URL itself: + +```javascript + async redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams, msalInstance) { + ... + ... + + try { + const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest); + res.redirect(authCodeUrlResponse); + } catch (error) { + next(error); + } + } +``` + +In the second leg of auth code flow uses, use the authorization code to request an ID token by using MSAL's [acquireTokenByCode]() method. You can store the ID token and user account information in an express session. + +```javascript + async handleRedirect(req, res, next) { + const authCodeRequest = { + ...req.session.authCodeRequest, + code: req.body.code, // authZ code + ... + }; + + try { + const msalInstance = this.getMsalInstance(this.config.msalConfig); + 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; + ... + ... + } catch (error) { + next(error); + } + } +``` + +### Update User information +When you the user to update the information like `display_name` or `email`, which can be done through the graphApi `patch` method [GraphApi - Update user](https://learn.microsoft.com/en-us/graph/api/user-update?view=graph-rest-1.0&tabs=http). +```javascript + fetch(graphEndpoint, req.session.accessToken, "PATCH", { + displayName: body.displayName, + mail: body.mail, + }) +``` +This update call is gated behind MFA and mfa is preformed through [conditional access](https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview), where we gate a specific scope in conditional access and when we need to perform MFA we call that scope. + + +#### Register the service app (mfa-app) + +1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure AD for Customers** service. +1. Select the **App Registrations** blade on the left, then select **New registration**. +1. In the **Register an application page** that appears, enter your application's registration information: + 1. In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `mfa-app`. + 1. Under **Supported account types**, select **Accounts in this organizational directory only** + 1. Select **Register** to create the application. +1. In the **Overview** blade, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. +1. In the app's registration screen, select the **Expose an API** blade to the left to open the page where you can publish the permission as an API for which client applications can obtain [access tokens](https://aka.ms/access-tokens) for. The first thing that we need to do is to declare the unique [resource](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) URI that the clients will be using to obtain access tokens for this API. To declare an resource URI(Application ID URI), follow the following steps: + 1. Select **Set** next to the **Application ID URI** to generate a URI that is unique for this app. + 1. For this sample, accept the proposed Application ID URI (`api://{clientId}`) by selecting **Save**. + > :information_source: Read more about Application ID URI at [Validation differences by supported account types (signInAudience)](https://docs.microsoft.com/azure/active-directory/develop/supported-accounts-validation). + +##### Publish Delegated Permissions + +1. All APIs must publish a minimum of one [scope](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code), also called [Delegated Permission](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#permission-types), for the client apps to obtain an access token for a *user* successfully. To publish a scope, follow these steps: +1. Select **Add a scope** button open the **Add a scope** screen and Enter the values as indicated below: + 1. For **Scope name**, use `User.MFA`. + 1. For **Admin consent display name** type in *User MFA action the 'mfa-app'*. + 1. For **Admin consent description** type in *e.g. Create a MFA action when User requests scope.*. + 1. Keep **State** as **Enabled**. + 1. Select the **Add scope** button on the bottom to save this scope. + 1. Repeat the steps above for another scope named **User.MFA** +1. Select the **Manifest** blade on the left. + 1. Set `accessTokenAcceptedVersion` property to **2**. + 1. Select on **Save**. + +> :information_source: Follow [the principle of least privilege when publishing permissions](https://learn.microsoft.com/security/zero-trust/develop/protected-api-example) for a web API. + +##### Publish Application Permissions + +1. All APIs should publish a minimum of one [App role for applications](https://docs.microsoft.com/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps#assign-app-roles-to-applications), also called [Application Permission](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#permission-types), for the client apps to obtain an access token as *themselves*, i.e. when they are not signing-in a user. **Application permissions** are the type of permissions that APIs should publish when they want to enable client applications to successfully authenticate as themselves and not need to sign-in users. To publish an application permission, follow these steps: +1. Still on the same app registration, select the **App roles** blade to the left. +1. Select **Create app role**: + 1. For **Display name**, enter a suitable name for your application permission, for instance **User.MFA**. + 1. For **Allowed member types**, choose **Application** to ensure other applications can be granted this permission. + 1. For **Value**, enter **User.MFA**. + 1. For **Description**, enter *Create a MFA action when User requests scope*. + 1. Select **Apply** to save your changes. + 1. Repeat the steps above for another app permission named **User.MFA**. +1. Go to Conditional access the the azure portal [App portal for conditioal access](https://entra.microsoft.com/?feature.msaljs=true#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies/fromNav/) + 1. Select **New Policy** + 1. Enter the name of the policy (e.g. "MFA Policy") + 1. Select **Users** and select thye group of users should have the mfa or selected users. + 1. Select **Target resources**, then **Select apps** selecte the **mfa-app**. + 1. Select **Grant** and select the `Grant access` and select the **Require multifactor authentication**. + 1. Select the Enable policy as **On** + 1. Select create. + +We can use this scope when requesting the update profile page so that if the User tries to open this page its asked to complete a MFA. + + +```javascript +router.get( + '/updateProfile', + isAuthenticated, // check if user is authenticated + authProvider.getToken(["User.ReadWrite", "api://{{clientId}}/user.mfa"]), // check for mfa + async function (req, res, next) { + const graphResponse = await fetch( + GRAPH_ME_ENDPOINT, + req.session.accessToken + ); + res.render("updateProfile", { + profile: graphResponse, + }); + } +); +``` + + +### Sign out + +When you want to sign the user out of the application, it isn't enough to end the user's session. You must redirect the user to the `logoutUri`. Otherwise, the user might be able to reauthenticate to your applications without reentering their credentials. If the name of your tenant is contoso, then the logoutUri looks similar to `https://contoso.ciamlogin.com/contoso.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000`. + +```javascript + async logout(req, res, next) { + /** + * Construct a logout URI and redirect the user to end the session with Microsoft Entra ID. + */ + const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`; + + req.session.destroy(() => { + res.redirect(logoutUri); + }); + } +``` + +### Deploying Web app to Azure App Service + +There is one web app in this sample. To deploy it to **Azure App Services**, you'll need to: + +*Create an **Azure App Service** +*Publish the projects to the **App Services**, and +*Update its client(s) to call the website instead of the local environment. + +#### Deploy your files of your web app + +1. In the **VS Code** activity bar, select the **Azure** logo to show the **Azure App Service** explorer. +1. Select **Sign in to Azure...**, then follow the instructions. Once signed in, the explorer should show the name of your **Azure** subscription(s). +1. On the **App Service** explorer section you see an upward-facing arrow icon. Select it publish your local files in the project folder to **Azure App Services** (use "Browse" option if needed, and locate the right folder). +1. Choose a creation option based on the operating system to which you want to deploy. In this sample, we illustrate by using the **Linux** option. +1. Select a **Node.js** version when prompted. We recommend a **LTS** version. +1. Type a globally unique name for your web app and select **Enter**. The name must be unique across all of **Azure** services. After you respond to all the prompts, **VS Code** shows the **Azure** resources that are being created for your app in its notification popup. +1. Select **Yes** when prompted to update your configuration. This action runs `npm install` on the target **Linux** server. + +#### Update app registration to use deployed app + +1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com) as at least an [Application Developer](https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#application-developer). +1. Browse to **Identity** >**Applications** > **App registrations**. +1. From the app registration list, select the app that you want to update. +1. Under **Manage**, select **Authentication**. +1. Update your **Redirect URIs** to to match the site URL of your Azure deployment such as `https://ciam-msal-node-webapp.azurewebsites.net/auth/redirect`. +1. Select **Configure** to save your changes. + +> :warning: If your app use *in-memory* storage, **Azure App Services** will spin down your web site if it is inactive. This action empties any records in the memory. In addition, if you increase the instance count of your website, Azure Service distributes the requests among the instances. Therefore, your app's records won't be the same on each instance. + + +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Learn More + +* [Customize the default branding](https://learn.microsoft.com/entra/external-id/customers/how-to-customize-branding-customers) +* [Language customize](https://learn.microsoft.com/entra/external-id/customers/how-to-customize-languages-customers) +* [Building Zero Trust ready apps](https://aka.ms/ztdevsession) +* [Initialize client applications using MSAL.js](https://learn.microsoft.com/entra/identity-platform/msal-js-initializing-client-applications) +* [Single sign-on with MSAL.js](https://learn.microsoft.com/entra/identity-platform/msal-js-sso) +* [Handle MSAL.js exceptions and errors](https://learn.microsoft.com/entra/msal/dotnet/advanced/exceptions/msal-error-handling?tabs=javascript) +* [Logging in MSAL.js applications](https://learn.microsoft.com/entra/msal/dotnet/advanced/exceptions/msal-logging?tabs=javascript) +* [Pass custom state in authentication requests using MSAL.js](https://learn.microsoft.com/entra/identity-platform/msal-js-pass-custom-state-authentication-request). diff --git a/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/admin-center-settings-icon.png b/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/admin-center-settings-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a3611e7b416f6a1c51df2e51f4e639b0e9552637 GIT binary patch literal 818 zcmV-21I_%2P)!S%) z(ZXX9F%ZIuio`@k)J{B&78Zek4^A1hP_R)nBUmV=5iA3eC@K;E0D}e#4L(pJJ~(O` ztyEGZC`v$+spbqB<6Q2=U$@t9?QgHW_Cr)vRpt0b8NL(tSD-A*`2BvCmzMz$LJ$sz znV+9WmgO&j|CE)L{rEFDFfhQ#$OubIOO%(FQ(0L_Lqh|(T#l`+EvBcZDK0K19*=(l z78P3M=H`ZYJkIX!E*BRU?C)%waF9$UaXOv2-R{B_+U<6{UN4%a;c~g~`~6?`DHseA3)OoC&2fHy&dSP4-sFji33_^ZC@Lx<91b%(JDcbC_4Q%1*%%)m&);%Q zO$`={1*6fZCl3z~BS{jyy}fjIcOyv>LqkKlZ!{XQSS(amSL-poLW9A8s;cz&_tW0q z4nRB}r=z2TbUF<{X=y33SPX~50l?ng9)UoBP$)z;oBez!@9*yjA@uRv+Z!JrANn|x z$?*F6`nwt-Xl-q!qM|~N>A=m+P0VI9ilXS|_VzYYQ&R{b5JKSd`G`iNy5H5+#naOh zYinztf!Ej9BoYZ;US4!_a&nS%I?dtXAtxs%WHK3iKA-MCKR=^s8j(oka}R54Ygt@e zrY2IU6sM=Bm`o;i zc6RiQW^iy2Ns{n* wJP09JUtj0$?v7wE$mQkbe*^wsefy976SX}KU<;LJZvX%Q07*qoM6N<$f|Po0YXATM literal 0 HcmV?d00001 diff --git a/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/screenshot.png b/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..02943172513d87b5972339bb645c2eb6ab34f7b5 GIT binary patch literal 12606 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 T3xB!K0oY!&yHIX@=h6QF+LGQ> literal 0 HcmV?d00001 diff --git a/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/topology.png b/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/topology.png new file mode 100644 index 0000000000000000000000000000000000000000..f5b02c046a36f4de105ca771f78b0bde45d93e62 GIT binary patch literal 22933 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 literal 0 HcmV?d00001 diff --git a/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/yes.png b/1-Authentication/7-sign-in-express-mfa/ReadmeFiles/yes.png new file mode 100644 index 0000000000000000000000000000000000000000..d2285c5c46cfb8c983a2a725f4ff13e241a5f319 GIT binary patch literal 614 zcmV-s0-61ZP)Mxgdo$d#k7bF$_Of$yBR1%&{?RX(S-St3z34+VrXLUxEO`o(2VC^ z&+dKC``+ikIsC3rO5tTmTbu{3118W0ECLx|N?K~XR#&)%N?U}1$3VPBECs}*rB?S1 zmA1GByabvx;(1^|T58NQRNA5u$N~}VQ$hi_EG_lbY5H7zU_=M#69$IQzbk`4QrhBx zYpAqE6u6_4?QTsFyE=~F2=7`QK(A})PI1q5ZRg z^H1P-gUOB71V0No-put^_M={)ZBB8!{R91Gn!^WM00`C{)YRo0a`@ zUZ9IkkzRO6sJ@B5s7*s4!p)1LGzL!cc0SJf@3}quy3mYMgz(MDvjB-=e++ih~EgalK(_1H>BM+G@)tWBhcwIC%->I;N$c9E4Ear zT6YO}<}}<)q!wTX2x%S^KmlFSQfa5DJ&~lPz5(}vzb=4}DuLkbFLMD%0_`e6c&_{XFn7~=ecbB33Xr4+-ZB*-T1Bh3d_?=3=T>t<807*qoM6N<$f|?{1 AbN~PV literal 0 HcmV?d00001 diff --git a/1-Authentication/7-sign-in-express-mfa/package-lock.json b/1-Authentication/7-sign-in-express-mfa/package-lock.json new file mode 100644 index 000000000..7398fa4fa --- /dev/null +++ b/1-Authentication/7-sign-in-express-mfa/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "7-sign-in-express-mfa", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}
ApplicationAppIdUrl in the Azure portal