Azure Active Directory B2C を使用して独自の Node.js Web API で認証を有効にする
この記事では、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 アプリと Web API を独自の Web API に置き換えます。
Visual Studio Code、または別のコード エディター
手順 1: 保護された Web API を作成する
次の手順に従って、Node.js Web API を作成します。
手順 1.1: プロジェクトを作成する
Node.js の場合は Express を使用して 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
ファイル内の次のコード スニペットをメモします。
passport Microsoft Entra ライブラリをインポートします
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 オプションを使用して passport Microsoft Entra ライブラリをインスタンス化します
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 ID プロバイダーに関する情報が含まれます。 Web API アプリではこの情報を使用して、Web アプリからベアラー トークンとして渡されるアクセス トークンが検証されます。
プロジェクトのルート フォルダーに
config.json
ファイルを作成してから、それに以下の JSON オブジェクトを追加します。{ "credentials": { "tenantName": "fabrikamb2c", "clientID": "93733604-cc77-4a3c-a604-87084dd55348" }, "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
ファイルで、以下のプロパティを更新します。
Section | キー | 値 |
---|---|---|
資格情報 | tenantName | Azure AD B2C テナント名の最初の部分 (例: fabrikamb2c )。 |
資格情報 | clientID | Web API アプリケーション ID。 Web API アプリケーションの登録 ID を取得する方法については、「前提条件」を参照してください。 |
policies | policyName | ユーザー フローまたはカスタム ポリシー。 ユーザー フローまたはポリシーを取得する方法については、「前提条件」を参照してください。 |
resource | scope | Web API アプリケーション登録のスコープ (例: [tasks.read] )。 Web API のスコープを取得する方法については、「前提条件」を参照してください。 |
手順 2: Web Node Web アプリケーションを作成する
次の手順に従って、Node Web アプリを作成します。 この Web アプリでは、ユーザーを認証して、手順 1 で作成した Node 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 フレームワークを使用して構築された UI が実装されています。signin.hbs
などのページ間で変更される UI は、{{{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}}
クラス属性を使用すると、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
サンプル Web アプリの構成に関するページで説明するように
.env
ファイルの値を変更します。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
: Web API で構成され、Web アプリに付与されるスコープであるwebApiScopes
プロパティ (この値は配列である必要があります) が含まれています。 また、呼び出される 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
: 簡易セッションの構成オブジェクト。getAuthCode
: ユーザーが資格情報とアプリケーションへの同意を入力できるようにする、認証要求の URL を作成するメソッド。 ConfidentialClientApplication クラスで定義されているgetAuthCodeUrl
メソッドを使用します。
高速ルート:
/
:- これは Web アプリへのエントリであり、
signin
ページをレンダリングします。
- これは Web アプリへのエントリであり、
/signin
:- ユーザーをサインインさせます。
getAuthCode()
メソッドを呼び出し、サインインとサインアップのユーザー フロー/ポリシー、APP_STATES.LOGIN
、apiConfig.webApiScopes
のauthority
をメソッドに渡します。- これにより、エンド ユーザーはログインを入力するように求められます。または、ユーザーがアカウントを持っていない場合は、サインアップすることができます。
- このエンドポイントからの最終的な応答には B2C から
/redirect
エンドポイントにポストバックされる認可コードが含まれます。
/redirect
:- Azure portal の Web アプリのリダイレクト URI として設定されたエンドポイントです。
- Azure AD B2C の応答で
state
クエリ パラメーターを使用して、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 をテストします。