教學課程:在 Node.js & Express Web 應用程式中登入使用者並取得 Microsoft Graph 的令牌
在本教學課程中,您會建置 Web 應用程式來登入使用者,並取得呼叫 Microsoft Graph 的存取令牌。 您建置的 Web 應用程式會針對 Node 使用Microsoft 驗證連結庫 (MSAL)。
請遵循本教學課程中的步驟來:
- 在 Azure 入口網站中註冊應用程式
- 建立 Express Web 應用程式專案
- 安裝驗證連結庫套件
- 新增應用程式註冊詳細數據
- 新增使用者登入的程序代碼
- 測試應用程式
如需詳細資訊,請參閱 範例程式代碼,示範如何使用 MSAL Node 登入、註銷及取得受保護資源的存取令牌,例如 Microsoft Graph。
先決條件
- Node.js
- Visual Studio Code 或其他程式碼編輯器
註冊應用程式
首先,完成 在 Microsoft 身分識別平臺註冊應用程式的步驟,然後 註冊您的應用程式。
針對您的應用程式註冊使用下列設定:
- 名稱:
ExpressWebApp
(建議) - 支援的帳戶類型:僅限此組織目錄中的帳戶
- 平台類型:Web
- 重新導向 URI:
http://localhost:3000/auth/redirect
- 用戶端密碼:
*********
(記錄此值以供後續步驟使用 - 只會顯示一次)
建立專案
使用 Express 應用程式產生器工具 建立應用程式基本架構。
- 首先,安裝 express-generator 套件:
npm install -g express-generator
- 然後,建立應用程式基本架構,如下所示:
express --view=hbs /ExpressWebApp && cd /ExpressWebApp
npm install
您現在有簡單的 Express Web 應用程式。 項目的檔案和資料夾結構看起來應該類似下列資料夾結構:
ExpressWebApp/
├── bin/
| └── wwww
├── public/
| ├── images/
| ├── javascript/
| └── stylesheets/
| └── style.css
├── routes/
| ├── index.js
| └── users.js
├── views/
| ├── error.hbs
| ├── index.hbs
| └── layout.hbs
├── app.js
└── package.json
安裝身份驗證庫
在終端機中找出項目目錄的根目錄,並透過 npm 安裝 MSAL 節點套件。
npm install --save @azure/msal-node
安裝其他相依套件
本教學課程中的 Web 應用程式範例會使用 express-session 套件來進行會話管理、dotenv 套件,以在開發期間讀取環境參數,以及 axios,以便對 Microsoft Graph API 進行網路呼叫。 透過 npm 安裝這些專案:
npm install --save express-session dotenv axios
新增應用程式註冊詳細數據
- 在項目資料夾的根目錄中建立 .env.dev 檔案。 然後新增下列程式代碼:
CLOUD_INSTANCE="Enter_the_Cloud_Instance_Id_Here" # cloud instance string should end with a trailing slash
TENANT_ID="Enter_the_Tenant_Info_Here"
CLIENT_ID="Enter_the_Application_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"
以您從 Azure 應用程式註冊入口網站取得的值填入這些詳細資料:
-
Enter_the_Cloud_Instance_Id_Here
:註冊應用程式的 Azure 雲端實例。- 針對主要(或 全域)的 Azure 雲端,請輸入
https://login.microsoftonline.com/
(包括末尾的斜線)。 - 針對 國家 雲端(例如中國),您可以在 國家雲端中找到適當的值。
- 針對主要(或 全域)的 Azure 雲端,請輸入
-
Enter_the_Tenant_Info_here
應該是下列其中一個參數:- 如果您的應用程式支援此組織目錄中的 帳戶,請將此值取代為 租使用者識別子 或 租使用者名稱。 例如,
contoso.microsoft.com
。 - 如果應用程式支援任何組織目錄中的 帳戶,請將此值取代為
organizations
。 - 如果您的應用程式支援任何組織目錄和個人Microsoft帳戶 中的帳戶,請將此值取代為
common
。 - 若要限制僅 個人Microsoft帳戶的支援,請將此值取代為
consumers
。
- 如果您的應用程式支援此組織目錄中的 帳戶,請將此值取代為 租使用者識別子 或 租使用者名稱。 例如,
-
Enter_the_Application_Id_Here
:您所註冊應用程式的 應用程式(用戶端)標識碼。 -
Enter_the_Client_secret
:將此值取代為您稍早建立的客戶端密碼。 若要產生新的金鑰,請在 Azure 入口網站的應用程式註冊設定中使用 憑證 & 秘密。
警告
原始程式碼中的任何純文字秘密都會造成更高的安全性風險。 本文僅使用純文本客戶端密碼來簡化。 使用 憑證認證,而不是機密用戶端應用程式中的用戶端密碼,特別是您想要部署到生產環境的應用程式。
-
Enter_the_Graph_Endpoint_Here
:您的應用程式將呼叫的 Microsoft Graph API 雲端實例。 針對主要(全域)Microsoft Graph API 服務,輸入https://graph.microsoft.com/
(包括後面的斜線)。 -
Enter_the_Express_Session_Secret_Here
用於簽署 Express 會話 Cookie 的密鑰。 選擇隨機字元字串,以取代此字串,例如您的客戶端密碼。
- 接下來,在專案的根目錄中建立名為 authConfig.js 的檔案,以讀取這些參數。 建立之後,請在該處新增下列程序代碼:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
require('dotenv').config({ path: '.env.dev' });
/**
* 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, // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID, // Full directory URL, in the form of https://login.microsoftonline.com/<tenant>
clientSecret: process.env.CLIENT_SECRET // Client secret generated from the app registration in Azure portal
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: 3,
}
}
}
const REDIRECT_URI = process.env.REDIRECT_URI;
const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI;
const GRAPH_ME_ENDPOINT = process.env.GRAPH_API_ENDPOINT + "v1.0/me";
module.exports = {
msalConfig,
REDIRECT_URI,
POST_LOGOUT_REDIRECT_URI,
GRAPH_ME_ENDPOINT
};
新增用戶登入和取得令牌的程式碼
- 建立名為 驗證的新資料夾,並在其中新增名為 AuthProvider.js 的新檔案。 這將包含 AuthProvider 類別,其會使用 MSAL 節點封裝必要的驗證邏輯。 在那裡新增下列程式代碼:
const msal = require('@azure/msal-node');
const axios = require('axios');
const { msalConfig } = require('../authConfig');
class AuthProvider {
msalConfig;
cryptoProvider;
constructor(msalConfig) {
this.msalConfig = msalConfig
this.cryptoProvider = new msal.CryptoProvider();
};
login(options = {}) {
return async (req, res, next) => {
/**
* MSAL Node library allows you to pass your custom state as state parameter in the Request object.
* The state parameter can also be used to encode information of the app's state before redirect.
* You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
*/
const state = this.cryptoProvider.base64Encode(
JSON.stringify({
successRedirect: options.successRedirect || '/',
})
);
const authCodeUrlRequestParams = {
state: state,
/**
* By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
scopes: options.scopes || [],
redirectUri: options.redirectUri,
};
const authCodeRequestParams = {
state: state,
/**
* By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
scopes: options.scopes || [],
redirectUri: options.redirectUri,
};
/**
* If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will
* make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making
* metadata discovery calls, thereby improving performance of token acquisition process. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md
*/
if (!this.msalConfig.auth.cloudDiscoveryMetadata || !this.msalConfig.auth.authorityMetadata) {
const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([
this.getCloudDiscoveryMetadata(this.msalConfig.auth.authority),
this.getAuthorityMetadata(this.msalConfig.auth.authority)
]);
this.msalConfig.auth.cloudDiscoveryMetadata = JSON.stringify(cloudDiscoveryMetadata);
this.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
}
const msalInstance = this.getMsalInstance(this.msalConfig);
// trigger the first leg of auth code flow
return this.redirectToAuthCodeUrl(
authCodeUrlRequestParams,
authCodeRequestParams,
msalInstance
)(req, res, next);
};
}
acquireToken(options = {}) {
return async (req, res, next) => {
try {
const msalInstance = this.getMsalInstance(this.msalConfig);
/**
* If a token cache exists in the session, deserialize it and set it as the
* cache for the new MSAL CCA instance. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
*/
if (req.session.tokenCache) {
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
}
const tokenResponse = await msalInstance.acquireTokenSilent({
account: req.session.account,
scopes: options.scopes || [],
});
/**
* On successful token acquisition, write the updated token
* cache back to the session. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
*/
req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.accessToken = tokenResponse.accessToken;
req.session.idToken = tokenResponse.idToken;
req.session.account = tokenResponse.account;
res.redirect(options.successRedirect);
} catch (error) {
if (error instanceof msal.InteractionRequiredAuthError) {
return this.login({
scopes: options.scopes || [],
redirectUri: options.redirectUri,
successRedirect: options.successRedirect || '/',
})(req, res, next);
}
next(error);
}
};
}
handleRedirect(options = {}) {
return async (req, res, next) => {
if (!req.body || !req.body.state) {
return next(new Error('Error: response not found'));
}
const authCodeRequest = {
...req.session.authCodeRequest,
code: req.body.code,
codeVerifier: req.session.pkceCodes.verifier,
};
try {
const msalInstance = this.getMsalInstance(this.msalConfig);
if (req.session.tokenCache) {
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
}
const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.idToken = tokenResponse.idToken;
req.session.account = tokenResponse.account;
req.session.isAuthenticated = true;
const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state));
res.redirect(state.successRedirect);
} catch (error) {
next(error);
}
}
}
logout(options = {}) {
return (req, res, next) => {
/**
* Construct a logout URI and redirect the user to end the
* session with Azure AD. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
*/
let logoutUri = `${this.msalConfig.auth.authority}/oauth2/v2.0/`;
if (options.postLogoutRedirectUri) {
logoutUri += `logout?post_logout_redirect_uri=${options.postLogoutRedirectUri}`;
}
req.session.destroy(() => {
res.redirect(logoutUri);
});
}
}
/**
* Instantiates a new MSAL ConfidentialClientApplication object
* @param msalConfig: MSAL Node Configuration object
* @returns
*/
getMsalInstance(msalConfig) {
return new msal.ConfidentialClientApplication(msalConfig);
}
/**
* 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
*/
redirectToAuthCodeUrl(authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
return async (req, res, next) => {
// Generate PKCE Codes before starting the authorization flow
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
// Set generated PKCE codes and method as session vars
req.session.pkceCodes = {
challengeMethod: 'S256',
verifier: verifier,
challenge: challenge,
};
/**
* By manipulating the request objects below before each request, we can obtain
* auth artifacts with desired claims. For more information, visit:
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationcoderequest
**/
req.session.authCodeUrlRequest = {
...authCodeUrlRequestParams,
responseMode: msal.ResponseMode.FORM_POST, // recommended for confidential clients
codeChallenge: req.session.pkceCodes.challenge,
codeChallengeMethod: req.session.pkceCodes.challengeMethod,
};
req.session.authCodeRequest = {
...authCodeRequestParams,
code: '',
};
try {
const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
res.redirect(authCodeUrlResponse);
} catch (error) {
next(error);
}
};
}
/**
* Retrieves cloud discovery metadata from the /discovery/instance endpoint
* @returns
*/
async getCloudDiscoveryMetadata(authority) {
const endpoint = 'https://login.microsoftonline.com/common/discovery/instance';
try {
const response = await axios.get(endpoint, {
params: {
'api-version': '1.1',
'authorization_endpoint': `${authority}/oauth2/v2.0/authorize`
}
});
return await response.data;
} catch (error) {
throw error;
}
}
/**
* Retrieves oidc metadata from the openid endpoint
* @returns
*/
async getAuthorityMetadata(authority) {
const endpoint = `${authority}/v2.0/.well-known/openid-configuration`;
try {
const response = await axios.get(endpoint);
return await response.data;
} catch (error) {
console.log(error);
}
}
}
const authProvider = new AuthProvider(msalConfig);
module.exports = authProvider;
- 接下來,在 路由 資料夾下建立名為 auth.js 的新檔案,並在該處新增下列程式代碼:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var express = require('express');
const authProvider = require('../auth/AuthProvider');
const { REDIRECT_URI, POST_LOGOUT_REDIRECT_URI } = require('../authConfig');
const router = express.Router();
router.get('/signin', authProvider.login({
scopes: [],
redirectUri: REDIRECT_URI,
successRedirect: '/'
}));
router.get('/acquireToken', authProvider.acquireToken({
scopes: ['User.Read'],
redirectUri: REDIRECT_URI,
successRedirect: '/users/profile'
}));
router.post('/redirect', authProvider.handleRedirect());
router.get('/signout', authProvider.logout({
postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI
}));
module.exports = router;
- 以下列代碼段取代現有的程式代碼,以更新 index.js 路由:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var express = require('express');
var 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,
});
});
module.exports = router;
- 最後,將現有的程式代碼取代為下列代碼段,以更新 users.js 路由:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var express = require('express');
var router = express.Router();
var fetch = require('../fetch');
var { GRAPH_ME_ENDPOINT } = 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('/profile',
isAuthenticated, // check if user is authenticated
async function (req, res, next) {
try {
const graphResponse = await fetch(GRAPH_ME_ENDPOINT, req.session.accessToken);
res.render('profile', { profile: graphResponse });
} catch (error) {
next(error);
}
}
);
module.exports = router;
新增程式代碼以呼叫 Microsoft Graph API
在專案的根目錄中建立名為 fetch.js 的檔案,並新增下列程式代碼:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var axios = require('axios');
/**
* Attaches a given access token to a MS Graph API call
* @param endpoint: REST API endpoint to call
* @param accessToken: raw access token string
*/
async function fetch(endpoint, accessToken) {
const options = {
headers: {
Authorization: `Bearer ${accessToken}`
}
};
console.log(`request made to ${endpoint} at: ` + new Date().toString());
try {
const response = await axios.get(endpoint, options);
return await response.data;
} catch (error) {
throw new Error(error);
}
}
module.exports = fetch;
新增檢視以顯示數據
- 在 [檢視] 資料夾中,更新 index.hbs 檔案,將現有的程式碼替換為以下內容:
<h1>{{title}}</h1>
{{#if isAuthenticated }}
<p>Hi {{username}}!</p>
<a href="/users/id">View ID token claims</a>
<br>
<a href="/auth/acquireToken">Acquire a token to call the Microsoft Graph API</a>
<br>
<a href="/auth/signout">Sign out</a>
{{else}}
<p>Welcome to {{title}}</p>
<a href="/auth/signin">Sign in</a>
{{/if}}
- 仍在相同的資料夾中,建立另一個名為 id.hbs 的檔案, 來顯示使用者標識元令牌的內容:
<h1>Azure AD</h1>
<h3>ID Token</h3>
<table>
<tbody>
{{#each idTokenClaims}}
<tr>
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
{{/each}}
</tbody>
</table>
<br>
<a href="https://aka.ms/id-tokens" target="_blank">Learn about claims in this ID token</a>
<br>
<a href="/">Go back</a>
- 最後,建立名為 profile.hbs 的另一個檔案,以顯示呼叫 Microsoft Graph 的結果:
<h1>Microsoft Graph API</h1>
<h3>/me endpoint response</h3>
<table>
<tbody>
{{#each profile}}
<tr>
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
{{/each}}
</tbody>
</table>
<br>
<a href="/">Go back</a>
註冊路由器並新增狀態管理
在專案資料夾根目錄中的 app.js 檔案中,註冊您稍早建立的路由,並使用 express-session 套件新增追蹤驗證狀態的會話支援。 將現有的程式代碼取代為下列代碼段:
/*
* 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,
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;
測試登入並呼叫 Microsoft Graph
您已完成應用程式的建立,現在已準備好測試應用程式的功能。
- 從項目資料夾的根目錄中執行下列命令,以啟動 Node.js 主控台應用程式:
npm start
- 開啟瀏覽器視窗並瀏覽至
http://localhost:3000
。 您應該會看到歡迎頁面:
的 Web 應用程式歡迎頁面
- 選取 登入 連結。 您應該會看到 Microsoft Entra 登入畫面:
顯示Microsoft Entra 登入畫面
- 輸入認證之後,您應該會看到同意畫面,要求您核准應用程式的許可權。
顯示Microsoft Entra 同意畫面
- 同意之後,您應該重新導向回應用程式首頁。
登入后,
- 選擇 [檢視 ID 令牌] 連結,以顯示已登入使用者的 ID 令牌內容。
的使用者標識元令牌畫面
- 返回首頁,然後選取 取得存取令牌並呼叫 Microsoft Graph API 連結。 完成之後,您應該會看到已登入使用者的 Microsoft Graph /me 端點回應。
顯示Graph 呼叫畫面
- 返回首頁,然後選取 [註銷] 連結。 您應該會看到 Microsoft Entra 註銷畫面。
顯示Microsoft Entra 註銷畫面
應用程式的運作方式
在本教學課程中,您會將 MSAL Node ConfidentialClientApplication 物件具現化,方法是傳遞組態物件 (msalConfig),其中包含從 Azure 入口網站上Microsoft Entra 應用程式註冊取得的參數。 您建立的 Web 應用程式會使用 OpenID Connect 通訊協定 來登入使用者和 OAuth 2.0 授權碼流程,以取得存取令牌。
後續步驟
如果您想要深入瞭解 Microsoft 身分識別平臺上的 Node.js & Express Web 應用程式開發,請參閱我們的多部分案例系列: