這很重要
自 2025 年 5 月 1 日起,Azure AD B2C 將不再可供新客戶購買。 在我們的常見問題中深入瞭解。
在本文中,您將瞭解如何建立呼叫 Web API 的 Web 應用程式。 Web API 必須受到 Azure Active Directory B2C (Azure AD B2C) 的保護。 若要授權存取 Web API,您可以提供包含 Azure AD B2C 所簽發之有效存取令牌的要求。
先決條件
在開始之前,請閱讀並完成使用 Azure AD B2C 在範例 Node.js Web API 中設定驗證的步驟。 然後,請遵循本文中的步驟,以您自己的 Web API 取代範例 Web 應用程式和 Web API。
Visual Studio Code 或其他程式碼編輯器
步驟 1:建立受保護的 Web API
請遵循下列步驟來建立您的 Node.js Web API。
步驟 1.1:建立專案
使用 Express for Node.js 建置 Web API。 若要建立 Web API,請執行下列動作:
- 建立名為
TodoList的新資料夾。 - 在資料夾下
TodoList,建立名為 的index.js檔案。 - 在命令提示字元中,執行
npm init -y。 此命令會為您的 Node.js 專案建立預設package.json檔案。 - 在命令提示字元中,執行
npm install express。 此命令會安裝 Express 架構。
步驟 1.2:安裝相依性
將驗證連結庫新增至您的 Web API 專案。 認證函式庫會解析 HTTP 認證標頭字段、驗證令牌,以及擷取聲明。 如需詳細資訊,請檢閱程式庫的文件。
若要新增驗證連結庫,請執行下列命令來安裝套件:
npm install passport
npm install passport-azure-ad
npm install morgan
morgan 套件是適用於 Node.js的 HTTP 要求記錄器中間件。
步驟 1.3:撰寫 Web API 伺服器程式代碼
在 index.js 檔案中,新增下列程式碼:
const express = require('express');
const morgan = require('morgan');
const passport = require('passport');
const config = require('./config.json');
const todolist = require('./todolist');
const cors = require('cors');
//<ms_docref_import_azuread_lib>
const BearerStrategy = require('passport-azure-ad').BearerStrategy;
//</ms_docref_import_azuread_lib>
global.global_todos = [];
//<ms_docref_azureadb2c_options>
const options = {
identityMetadata: `https://${config.credentials.tenantName}.b2clogin.com/${config.credentials.tenantName}.onmicrosoft.com/${config.policies.policyName}/${config.metadata.version}/${config.metadata.discovery}`,
clientID: config.credentials.clientID,
audience: config.credentials.clientID,
policyName: config.policies.policyName,
isB2C: config.settings.isB2C,
validateIssuer: config.settings.validateIssuer,
loggingLevel: config.settings.loggingLevel,
passReqToCallback: config.settings.passReqToCallback
}
//</ms_docref_azureadb2c_options>
//<ms_docref_init_azuread_lib>
const bearerStrategy = new BearerStrategy(options, (token, done) => {
// Send user info using the second argument
done(null, { }, token);
}
);
//</ms_docref_init_azuread_lib>
const app = express();
app.use(express.json());
//enable CORS (for testing only -remove in production/deployment)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Authorization, Origin, X-Requested-With, Content-Type, Accept');
next();
});
app.use(morgan('dev'));
app.use(passport.initialize());
passport.use(bearerStrategy);
// To do list endpoints
app.use('/api/todolist', todolist);
//<ms_docref_protected_api_endpoint>
// API endpoint, one must present a bearer accessToken to access this endpoint
app.get('/hello',
passport.authenticate('oauth-bearer', {session: false}),
(req, res) => {
console.log('Validated claims: ', req.authInfo);
// Service relies on the name claim.
res.status(200).json({'name': req.authInfo['name']});
}
);
//</ms_docref_protected_api_endpoint>
//<ms_docref_anonymous_api_endpoint>
// API anonymous endpoint, returns a date to the caller.
app.get('/public', (req, res) => res.send( {'date': new Date() } ));
//</ms_docref_anonymous_api_endpoint>
const port = process.env.PORT || 5000;
app.listen(port, () => {
console.log('Listening on port ' + port);
});
請注意 index.js檔案中的這些代碼片段:
匯入 Microsoft Entra 的 passport 程式庫
const BearerStrategy = require('passport-azure-ad').BearerStrategy;設定 Azure AD B2C 選項
const options = { identityMetadata: `https://${config.credentials.tenantName}.b2clogin.com/${config.credentials.tenantName}.onmicrosoft.com/${config.policies.policyName}/${config.metadata.version}/${config.metadata.discovery}`, clientID: config.credentials.clientID, audience: config.credentials.clientID, policyName: config.policies.policyName, isB2C: config.settings.isB2C, validateIssuer: config.settings.validateIssuer, loggingLevel: config.settings.loggingLevel, passReqToCallback: config.settings.passReqToCallback }使用 Azure AD B2C 選項實例化 Microsoft Entra passport 庫
const bearerStrategy = new BearerStrategy(options, (token, done) => { // Send user info using the second argument done(null, { }, token); } );受保護的 API 端點。 它負責處理包含有效 Azure AD B2C 簽發存取權杖的要求。 此端點會傳回存取令牌內
name聲明的值。// API endpoint, one must present a bearer accessToken to access this endpoint app.get('/hello', passport.authenticate('oauth-bearer', {session: false}), (req, res) => { console.log('Validated claims: ', req.authInfo); // Service relies on the name claim. res.status(200).json({'name': req.authInfo['name']}); } );匿名 API 端點。 Web 應用程式可以呼叫它,而不顯示存取令牌。 使用它透過匿名呼叫來偵錯您的 Web API。
// API anonymous endpoint, returns a date to the caller. app.get('/public', (req, res) => res.send( {'date': new Date() } ));
步驟 1.4:設定 Web API
將設定新增至設定檔。 檔案包含 Azure AD B2C 識別提供者的相關信息。 Web API 應用程式會使用此資訊來驗證由 Web 應用程式傳遞作為持有者代幣的存取令牌。
在專案根資料夾底下,建立檔案
config.json,然後將它新增至下列 JSON 物件:{ "credentials": { "tenantName": "fabrikamb2c", "clientID": "Enter_the_Application_Id_Here" }, "policies": { "policyName": "B2C_1_susi" }, "resource": { "scope": ["tasks.read"] }, "metadata": { "authority": "login.microsoftonline.com", "discovery": ".well-known/openid-configuration", "version": "v2.0" }, "settings": { "isB2C": true, "validateIssuer": true, "passReqToCallback": false, "loggingLevel": "info" } }在
config.json檔案中,更新下列屬性:
| 章節 | 鑰匙 | 價值觀 |
|---|---|---|
| 資格證明 | 租戶名稱 | Azure AD B2C 租用者名稱 的第一個部分(例如fabrikamb2c)。 |
| 資格證明 | 用戶識別碼 (clientID) | Web API 應用程式識別碼。 若要瞭解如何取得 Web API 應用程式註冊識別碼,請參閱 必要條件。 |
| 政策 | 政策名稱 | 使用者流程或自定義原則。 如需了解如何取得您的使用者流程或原則,請參閱必要條件。 |
| 資源 | 範圍 | Web API 應用程式註冊的範圍,例如 [tasks.read]。 若要瞭解如何取得 Web API 範圍,請參閱 必要條件。 |
步驟 2:建立 Web 節點 Web 應用程式
請遵循下列步驟來建立節點 Web 應用程式。 此 Web 應用程式會驗證使用者以取得存取權杖,以用來呼叫您在 步驟 1 中建立的節點 Web API:
步驟 2.1:建立節點專案
建立資料夾來儲存節點應用程式,例如 call-protected-api。
在您的終端機中,將目錄變更為節點應用程式資料夾,例如
cd call-protected-api,然後執行npm init -y。 此命令會為您的 Node.js 專案建立預設 package.json 檔案。在終端中執行
npm install express。 此命令會安裝 Express 架構。建立更多資料夾與檔案以達成下列項目結構:
call-protected-api/ ├── index.js └── package.json └── .env └── views/ └── layouts/ └── main.hbs └── signin.hbs └── api.hbs資料夾
views包含 Web 應用程式 UI 的句柄欄檔案。
步驟 2.2:安裝相依性
在您的終端機中,執行下列命令安裝 dotenv、express-handlebars、express-session 和 @azure/msal-node 套件:
npm install dotenv
npm install express-handlebars
npm install express
npm install axios
npm install express-session
npm install @azure/msal-node
步驟 2.3:建置 Web 應用程式 UI 元件
在
main.hbs檔案中,新增下列程式碼:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <title>Azure AD B2C | Enable authenticate on web API using MSAL for B2C</title> <!-- adding Bootstrap 4 for UI components --> <!-- CSS only --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> <link rel="SHORTCUT ICON" href="https://c.s-microsoft.com/favicon.ico?v2" type="image/x-icon"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <a class="navbar-brand" href="/">Microsoft Identity Platform</a> {{#if showSignInButton}} <div class="ml-auto"> <a type="button" id="SignIn" class="btn btn-success" href="/signin" aria-haspopup="true" aria-expanded="false"> Sign in to call PROTECTED API </a> <a type="button" id="SignIn" class="btn btn-warning" href="/api" aria-haspopup="true" aria-expanded="false"> Or call the ANONYMOUS API </a> </div> {{else}} <p class="navbar-brand d-flex ms-auto">Hi {{givenName}}</p> <a class="navbar-brand d-flex ms-auto" href="/signout">Sign out</a> {{/if}} </nav> <br> <h5 class="card-header text-center">MSAL Node Confidential Client application with Auth Code Flow</h5> <br> <div class="row" style="margin:auto" > {{{body}}} </div> <br> <br> </body> </html>檔案
main.hbs位於layout資料夾中,它應該包含應用程式內所需的任何 HTML 程式代碼。 它會實作使用 Bootstrap 5 CSS Framework 建置的 UI。 任何從頁面變更的 UI,例如signin.hbs,都會放在顯示為{{{body}}}的佔位符中。在
signin.hbs檔案中,新增下列程式碼:<div class="col-md-3" style="margin:auto"> <div class="card text-center"> <div class="card-body"> {{#if showSignInButton}} {{else}} <h5 class="card-title">You have signed in</h5> <a type="button" id="Call-api" class="btn btn-success" href="/api" aria-haspopup="true" aria-expanded="false"> Call the PROTECTED API </a> {{/if}} </div> </div> </div> </div>在
api.hbs檔案中,新增下列程式碼:<div class="col-md-3" style="margin:auto"> <div class="card text-center bg-{{bg_color}}"> <div class="card-body"> <h5 class="card-title">{{data}}</h5> </div> </div> </div>此頁面會顯示來自 API 的回應。
bg-{{bg_color}}Bootstrap 卡片中的類別屬性可讓 UI 顯示不同 API 端點的不同背景色彩。
步驟 2.4:完成 Web 應用程式伺服器程式代碼
在 檔案中
.env,新增下列程序代碼,其中包括伺服器 HTTP 埠、應用程式註冊詳細數據,以及登入和註冊使用者流程/原則詳細數據:SERVER_PORT=3000 #web apps client ID APP_CLIENT_ID=<You app client ID here> #session secret SESSION_SECRET=sessionSecretHere #web app client secret APP_CLIENT_SECRET=<Your app client secret here> #tenant name TENANT_NAME=<your-tenant-name> #B2C sign up and sign in user flow/policy name and authority SIGN_UP_SIGN_IN_POLICY_AUTHORITY=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name> AUTHORITY_DOMAIN=https://<your-tenant-name>.b2clogin.com #client redorect url APP_REDIRECT_URI=http://localhost:3000/redirect LOGOUT_ENDPOINT=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name>/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000修改檔案中的
.env值,如設定範例 Web 應用程式中所述在您的
index.js檔案中,新增下列程序代碼:/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ require('dotenv').config(); const express = require('express'); const session = require('express-session'); const {engine} = require('express-handlebars'); const msal = require('@azure/msal-node'); //Use axios to make http calls const axios = require('axios'); //<ms_docref_configure_msal> /** * Confidential Client Application Configuration */ const confidentialClientConfig = { auth: { clientId: process.env.APP_CLIENT_ID, authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, clientSecret: process.env.APP_CLIENT_SECRET, knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array redirectUri: process.env.APP_REDIRECT_URI, validateAuthority: false }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { console.log(message); }, piiLoggingEnabled: false, logLevel: msal.LogLevel.Verbose, } } }; // Initialize MSAL Node const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig); //</ms_docref_configure_msal> // Current web API coordinates were pre-registered in a B2C tenant. //<ms_docref_api_config> const apiConfig = { webApiScopes: [`https://${process.env.TENANT_NAME}.onmicrosoft.com/tasks-api/tasks.read`], anonymousUri: 'http://localhost:5000/public', protectedUri: 'http://localhost:5000/hello' }; //</ms_docref_api_config> /** * The MSAL.js library allows you to pass your custom state as state parameter in the Request object * By default, MSAL.js passes a randomly generated unique state parameter value in the authentication requests. * 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. * For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request */ const APP_STATES = { LOGIN: 'login', CALL_API:'call_api' } /** * Request Configuration * We manipulate these two request objects below * to acquire a token with the appropriate claims. */ const authCodeRequest = { redirectUri: confidentialClientConfig.auth.redirectUri, }; const tokenRequest = { redirectUri: confidentialClientConfig.auth.redirectUri, }; /** * Using express-session middleware. Be sure to familiarize yourself with available options * and set them as desired. Visit: https://www.npmjs.com/package/express-session */ const sessionConfig = { secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: false, // set this to true on production } } //Create an express instance const app = express(); //Set handlebars as your view engine app.engine('.hbs', engine({extname: '.hbs'})); app.set('view engine', '.hbs'); app.set("views", "./views"); app.use(session(sessionConfig)); /** * This method is used to generate an auth code request * @param {string} authority: the authority to request the auth code from * @param {array} scopes: scopes to request the auth code for * @param {string} state: state of the application, tag a request * @param {Object} res: express middleware response object */ const getAuthCode = (authority, scopes, state, res) => { // prepare the request console.log("Fetching Authorization code") authCodeRequest.authority = authority; authCodeRequest.scopes = scopes; authCodeRequest.state = state; //Each time you fetch Authorization code, update the authority in the tokenRequest configuration tokenRequest.authority = authority; // request an authorization code to exchange for a token return confidentialClientApplication.getAuthCodeUrl(authCodeRequest) .then((response) => { console.log("\nAuthCodeURL: \n" + response); //redirect to the auth code URL/send code to res.redirect(response); }) .catch((error) => { res.status(500).send(error); }); } app.get('/', (req, res) => { res.render('signin', { showSignInButton: true }); }); app.get('/signin',(req, res)=>{ //Initiate a Auth Code Flow >> for sign in //Pass the api scopes as well so that you received both the IdToken and accessToken getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,apiConfig.webApiScopes, APP_STATES.LOGIN, res); }); app.get('/redirect',(req, res)=>{ if (req.query.state === APP_STATES.LOGIN) { // prepare the request for calling the web API tokenRequest.authority = process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY; tokenRequest.scopes = apiConfig.webApiScopes; tokenRequest.code = req.query.code; confidentialClientApplication.acquireTokenByCode(tokenRequest) .then((response) => { req.session.accessToken = response.accessToken; req.session.givenName = response.idTokenClaims.given_name; console.log('\nAccessToken:' + req.session.accessToken); res.render('signin', {showSignInButton: false, givenName: response.idTokenClaims.given_name}); }).catch((error) => { console.log(error); res.status(500).send(error); }); }else{ res.status(500).send('We do not recognize this response!'); } }); //<ms_docref_api_express_route> app.get('/api', async (req, res) => { if(!req.session.accessToken){ //User is not logged in and so they can only call the anonymous API try { const response = await axios.get(apiConfig.anonymousUri); console.log('API response' + response.data); res.render('api',{data: JSON.stringify(response.data), showSignInButton: true, bg_color:'warning'}); } catch (error) { console.error(error); res.status(500).send(error); } }else{ //Users have the accessToken because they signed in and the accessToken is still in the session console.log('\nAccessToken:' + req.session.accessToken); let accessToken = req.session.accessToken; const options = { headers: { //accessToken used as bearer token to call a protected API Authorization: `Bearer ${accessToken}` } }; try { const response = await axios.get(apiConfig.protectedUri, options); console.log('API response' + response.data); res.render('api',{data: JSON.stringify(response.data), showSignInButton: false, bg_color:'success', givenName: req.session.givenName}); } catch (error) { console.error(error); res.status(500).send(error); } } }); //</ms_docref_api_express_route> /** * Sign out end point */ app.get('/signout',async (req, res)=>{ logoutUri = process.env.LOGOUT_ENDPOINT; req.session.destroy(() => { res.redirect(logoutUri); }); }); app.listen(process.env.SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port !` + process.env.SERVER_PORT));檔案中的
index.js程式代碼包含全域變數和快速路由。全域變數:
confidentialClientConfig:MSAL 組態物件,用來建立機密用戶端應用程式物件。/** * Confidential Client Application Configuration */ const confidentialClientConfig = { auth: { clientId: process.env.APP_CLIENT_ID, authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, clientSecret: process.env.APP_CLIENT_SECRET, knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array redirectUri: process.env.APP_REDIRECT_URI, validateAuthority: false }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { console.log(message); }, piiLoggingEnabled: false, logLevel: msal.LogLevel.Verbose, } } }; // Initialize MSAL Node const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);apiConfig:包含webApiScopes屬性(其值必須是數位),這是 Web API 中設定的範圍,並授與 Web 應用程式。 它也具有要呼叫之 Web API 的 URI,也就是anonymousUri和protectedUri。const apiConfig = { webApiScopes: [`https://${process.env.TENANT_NAME}.onmicrosoft.com/tasks-api/tasks.read`], anonymousUri: 'http://localhost:5000/public', protectedUri: 'http://localhost:5000/hello' };APP_STATES:在請求中包含的值,這個值也會在令牌回應中返回。 用來區分從 Azure AD B2C 收到的回應。authCodeRequest:用來擷取授權碼的組態物件。tokenRequest:組態物件,用來依授權碼取得令牌。sessionConfig:Express 會話的組態物件。getAuthCode:建立授權要求URL的方法,讓使用者輸入認證並同意應用程式。 它會使用getAuthCodeUrl方法,該方法是在ConfidentialClientApplication類別中定義的。
快速路由:
-
/:- 它是 Web 應用程式的入口,並呈現
signin頁面。
- 它是 Web 應用程式的入口,並呈現
-
/signin:- 登入使用者。
- 呼叫
getAuthCode()方法,並將 [authority使用者流程/原則] 和APP_STATES.LOGIN傳遞給apiConfig.webApiScopes它。 - 這會令使用者被要求輸入其登入資訊,或者如果使用者沒有賬號,他們可以註冊。
- 此端點產生的最終回應包含授權碼,該授權碼從 B2C 回傳到
/redirect端點。
-
/redirect:- 在 Azure 入口網站中,這是設定為 Web 應用程式的 重新導向 URI 的端點。
- 它會使用
stateAzure AD B2C 回應中的查詢參數,區分從 Web 應用程式提出的要求。 - 如果應用程式狀態為
APP_STATES.LOGIN,則會使用取得的授權碼來使用acquireTokenByCode()方法來擷取令牌。 使用acquireTokenByCode方法要求令牌時,您會在取得授權碼時使用相同的範圍。 取得的權杖包含accessToken、idToken與idTokenClaims。 取得accessToken之後,您會將它放在會話中,以供稍後用來呼叫 Web API。
-
/api:- 呼叫 Web API。
-
accessToken如果 不在會話中,請呼叫匿名 API 端點 (http://localhost:5000/public),否則呼叫受保護的 API 端點 (http://localhost:5000/hello)。
-
/signout:- 註銷使用者。
- 清除 Web 應用程式的工作階段,並對 Azure AD B2C 登出端點進行 HTTP 呼叫。
步驟 3:執行 Web 應用程式和 API
請遵循 執行 Web 應用程式和 API 中的步驟來測試 Web 應用程式和 Web API。