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 によって発行された有効なアクセス トークンを含む要求を処理します。

必須コンポーネント

手順 1: 保護された Web API を作成する

次の手順に従って、Node.js Web API を作成します。

手順 1.1: プロジェクトを作成する

Node.js の場合は Express を使用して Web API を作成します。 Web API を作成するには、以下を実行します。

  1. TodoList という名前の新しいフォルダーを作成します。
  2. TodoList フォルダーの下に、index.js という名前のファイルを作成します。
  3. コマンド シェルで npm init -y を実行します。 このコマンドは、Node.js プロジェクトのデフォルトの package.json ファイルを作成します。
  4. コマンド シェルで 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 アプリからベアラー トークンとして渡されるアクセス トークンが検証されます。

  1. プロジェクトのルート フォルダーに 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"
        }
    }
    
  2. 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)。

  1. ターミナルで、cd call-protected-api などのノード アプリ フォルダーにディレクトリを変更し、npm init -y を実行します。 このコマンドを実行すると、Node.js プロジェクト用に既定の package.json ファイルが作成されます。

  2. ターミナルで、npm install express を実行します。 このコマンドで、Express フレームワークがインストールされます。

  3. 次のプロジェクト構造を実現するために、さらに多くのフォルダーとファイルを作成します。

    call-protected-api/
    ├── index.js
    └── package.json
    └── .env
    └── views/
        └── layouts/
            └── main.hbs
        └── signin.hbs
        └── api.hbs
    

    views フォルダーには、Web アプリの UI のハンドルバー ファイルが含まれています。

ステップ 2.2: 依存関係をインストールする

ターミナルで、次のコマンドを実行して、 dotenvexpress-handlebarsexpress-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 コンポーネントをビルドする

  1. 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}}} として表示されるプレースホルダーに配置されます。

  2. 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>
    
  3. 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 アプリケーション サーバー コードを完了する

  1. .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 ファイルの値を変更します。

  2. 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 (つまり、anonymousUriprotectedUri) も含まれます。

      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 ページをレンダリングします。
    • /signin:
      • ユーザーをサインインさせます。
      • getAuthCode() メソッドを呼び出し、サインインとサインアップのユーザー フロー/ポリシー、APP_STATES.LOGINapiConfig.webApiScopesauthority をメソッドに渡します。
      • これにより、エンド ユーザーはログインを入力するように求められます。または、ユーザーがアカウントを持っていない場合は、サインアップすることができます。
      • このエンドポイントからの最終的な応答には B2C から /redirect エンドポイントにポストバックされる認可コードが含まれます。
    • /redirect:
      • Azure portal の Web アプリのリダイレクト URI として設定されたエンドポイントです。
      • Azure AD B2C の応答で state クエリ パラメーターを使用して、Web アプリから行われた要求を区別します。
      • アプリの状態が APP_STATES.LOGIN の場合は、取得された認可コードは、acquireTokenByCode() メソッドを使用したトークンの取得に使用されます。 acquireTokenByCode メソッドを使用してトークンを要求するときは、認証コードの取得時に使用したものと同じスコープを使用します。 取得したトークンには、accessTokenidTokenidTokenClaims が含まれます。 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 をテストします。

次のステップ