适用于: 员工租户
外部租户(了解详细信息)
在本教程中,将登录和注销逻辑添加到 Node/Express Web 应用。 通过此代码,可以在外部租户或工作人员租户中让用户登录到面向客户的应用。
本教程是 3 部分教程系列的第 2 部分。
在本教程中,你将:
- 添加登录和注销逻辑
- 查看 ID 令牌声明
- 运行应用并测试登录和注销体验。
先决条件
创建 MSAL 配置对象
在代码编辑器中,打开 authConfig.js 文件,然后添加以下代码:
require('dotenv').config();
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';
const GRAPH_ME_ENDPOINT = process.env.GRAPH_API_ENDPOINT + "v1.0/me" || 'Enter_the_Graph_Endpoint_Here';
/**
* 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
//For external tenant
authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // replace "Enter_the_Tenant_Subdomain_Here" with your tenant name
//For workforce tenant
//authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID
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',
},
},
};
module.exports = {
msalConfig,
REDIRECT_URI,
POST_LOGOUT_REDIRECT_URI,
TENANT_SUBDOMAIN,
GRAPH_ME_ENDPOINT
};
msalConfig
对象包含一组用于自定义身份验证流行为的配置选项。
在 authConfig.js 文件中替换以下项:
将
Enter_the_Application_Id_Here
替换为之前注册的应用的应用程序(客户端)ID。Enter_the_Tenant_Subdomain_Here
并将其替换为外部目录(租户)子域。 例如,如果租户主域名是contoso.onmicrosoft.com
,请使用contoso
。 如果没有租户名称,请了解如何读取租户详细信息。 此值仅适用于外部租户。将
Enter_the_Client_Secret_Here
替换为之前复制的应用机密值。将
Enter_the_Graph_Endpoint_Here
替换为应用将调用的 Microsoft Graph API 云实例。 使用值https://graph.microsoft.com/
(包括最后的斜杠)
如果使用 .env 文件存储配置信息:
在代码编辑器中,打开 .env 文件,然后添加以下代码。
CLIENT_ID=Enter_the_Application_Id_Here TENANT_SUBDOMAIN=Enter_the_Tenant_Subdomain_Here CLOUD_INSTANCE="Enter_the_Cloud_Instance_Id_Here" # cloud instance string should end with a trailing slash TENANT_ID=Enter_the_Tenant_ID_here CLIENT_SECRET=Enter_the_Client_Secret_Here REDIRECT_URI=http://localhost:3000/auth/redirect POST_LOGOUT_REDIRECT_URI=http://localhost:3000 GRAPH_API_ENDPOINT=Enter_the_Graph_Endpoint_Here # graph api endpoint string should end with a trailing slash EXPRESS_SESSION_SECRET=Enter_the_Express_Session_Secret_Here # express session secret, just any random text
替换占位符:
- 如前所述,
Enter_the_Application_Id_Here
、Enter_the_Tenant_Subdomain_Here
和Enter_the_Client_Secret_Here
。 - 将
Enter_the_Cloud_Instance_Id_Here
替换为应用程序注册到的 Azure 云实例。 使用https://login.microsoftonline.com/
作为其值(包括结尾的正斜杠)。 此值仅适用于员工租户。 - 将
Enter_the_Tenant_ID_here
替换为员工租户 ID 或主域,如 aaaabbbb-0000-cccc-1111-dddd2222eeee 或 contoso.microsoft.com。 此值仅适用于员工租户。
- 如前所述,
你在msalConfig
文件中导出REDIRECT_URI
、TENANT_SUBDOMAIN
、GRAPH_ME_ENDPOINT
、POST_LOGOUT_REDIRECT_URI
和变量,使其在其他文件中可访问。
应用的授权 URL
外部租户和员工租户的应用权限有所不同。 构建它们,如下所示:
//Authority for workforce tenant
authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID
使用自定义 URL 域(可选)
添加快速路由
快速路由提供支持执行操作(例如登录、退出登录和查看 ID 令牌声明)的终结点。
应用入口点
在代码编辑器中,打开 routes/index.js 文件,然后添加以下代码:
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;
/
路由是应用程序的入口点。 它呈现之前在生成应用 UI 组件中创建的视图/index.hbs 视图。 isAuthenticated
是一个布尔变量,用于确定在视图中看到的内容。
登录和登出
在代码编辑器中,打开 路由/auth.js 文件,然后添加以下代码:
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;
在代码编辑器中,打开 控制器/authController.js 文件,然后添加以下代码:
const authProvider = require('../auth/AuthProvider'); exports.signIn = async (req, res, next) => { return authProvider.login(req, res, next); }; exports.handleRedirect = async (req, res, next) => { return authProvider.handleRedirect(req, res, next); } exports.signOut = async (req, res, next) => { return authProvider.logout(req, res, next); };
在代码编辑器中,打开 身份验证/AuthProvider.js 文件,然后添加以下代码:
const msal = require('@azure/msal-node'); const axios = require('axios'); const { msalConfig, TENANT_SUBDOMAIN, REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, GRAPH_ME_ENDPOINT} = 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: [], }; 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: [], }; /** * 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.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); } } async logout(req, res, next) { /** * Construct a logout URI and redirect the user to end the * session with Microsoft Entra ID. For more information, visit: * https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request */ //For external tenant //const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`; //For workforce tenant let logoutUri = `${this.config.msalConfig.auth.authority}/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() { // For external tenant //const endpoint = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/v2.0/.well-known/openid-configuration`; // For workforce tenant const endpoint = `${this.config.msalConfig.auth.authority}/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;
/signin
、/signout
和/redirect
路由在路由/auth.js 文件中定义,但在身份验证/AuthProvider.js 类中实现其逻辑。
login
方法处理/signin
路由:它通过触发身份验证代码流的第一回合来启动登录流。
它使用之前创建的 MSAL 配置对象初始化
msalConfig
实例。const msalInstance = this.getMsalInstance(this.config.msalConfig);
该方法
getMsalInstance
定义为:getMsalInstance(msalConfig) { return new msal.ConfidentialClientApplication(msalConfig); }
授权代码流的第一个分支会生成授权代码请求 URL,然后重定向到该 URL 以获取授权代码。 第一个分支在
redirectToAuthCodeUrl
方法中实现。 注意我们如何使用 MSAL 的 getAuthCodeUrl 方法生成授权代码 URL://... const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest); //...
然后重定向到授权代码 URL 本身。
//... res.redirect(authCodeUrlResponse); //...
handleRedirect
方法处理/redirect
路由:在快速入门:在示例 Web 应用中登录用户前面的 Microsoft Entra 管理中心中将此 URL 设置为 Web 应用的重定向 URI。
此终结点实现授权代码流的第二个分支。 它使用授权代码通过 MSAL 的 acquireTokenByCode 方法请求 ID 令牌。
//... const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body); //...
收到响应后,你可以创建一个 Express 会话并在其中存储想要的任何信息。 你需要将
isAuthenticated
包括在内并将其设置为true
://... req.session.idToken = tokenResponse.idToken; req.session.account = tokenResponse.account; req.session.isAuthenticated = true; //...
logout
方法处理/signout
路由: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); }); }
它发起退出登录请求。
如果想要将用户从应用程序中退出登录,只是终止用户的会话是不够的。 必须将用户重定向到 logoutUri。 否则,用户可能可以向应用程序重新进行身份验证,且无需重新输入其凭据。 如果租户的名称为 contoso,则 logoutUri 看起来类似于
https://contoso.ciamlogin.com/contoso.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000
。
应用的注销 URI 和授权元数据终结点
应用的注销 URI、logoutUri
和 endpoint
(用于外部和员工租户)的颁发机构元数据终结点有所不同。 构建它们,如下所示:
//Logout URI for workforce tenant
const logoutUri = `${this.config.msalConfig.auth.authority}/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
//authority metadata endpoint for workforce tenant
const endpoint = `${this.config.msalConfig.auth.authority}/v2.0/.well-known/openid-configuration`;
查看 ID 令牌声明
在代码编辑器中,打开 routes/users.js 文件,然后添加以下代码:
const express = require('express');
const router = express.Router();
// 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 });
}
);
module.exports = router;
如果用户已经过身份验证,则 /id
路由使用 views/id.hbs 视图显示 ID 令牌声明。 你之前在生成应用 UI 组件中添加了此视图。
提取特定的 ID 令牌声明,例如“给定名称”:
const givenName = req.session.account.idTokenClaims.given_name
完善你的网页应用程序
在代码编辑器中,打开 app.js 文件,然后将 代码从app.js 添加到其中。
在代码编辑器中,打开 server.js 文件,然后将 代码从server.js 添加到其中。
在代码编辑器中,打开 package.json 文件,然后将属性更新
scripts
为:"scripts": { "start": "node server.js" }
运行并测试 Node/Express.js Web 应用
此时,可以测试节点 Web 应用。
使用 “创建新用户 ”中的步骤在员工租户中创建测试用户。 如果你无权访问租户,请让租户管理员为你创建用户。
若要启动服务器,请从项目目录中运行以下命令:
npm start
打开浏览器,然后转到
http://localhost:3000
。 应会看到类似于以下屏幕截图的页面:选择“登录”以启动登录过程。 首次登录时,系统会提示你提供同意,以允许应用程序登录和访问配置文件,如以下屏幕截图所示:
成功登录后,你将被重定向回应用程序主页。