次の方法で共有


Azure Active Directory B2C を使用して独自の Node.js Web API で認証を有効にする

重要

2025 年 5 月 1 日より、Azure AD B2C は新規のお客様向けに購入できなくなります。 詳細については、FAQ を参照してください

この記事では、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: プロジェクトを作成する

Express for Node.js を使用して 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

モルガン パッケージは、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 ライブラリをインポートします

    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": "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"
        }
    }
    
  2. config.json ファイルで、次のプロパティを更新します。

セクション 価値
資格情報 テナント名 Azure AD B2C テナント名 の最初の部分 (たとえば、 fabrikamb2c)。
資格情報 クライアントID Web API アプリケーション ID。 Web API アプリケーション登録 ID を取得する方法については、「 前提条件」を参照してください。
ポリシー ポリシー名 ユーザー フロー、またはカスタム ポリシー。 ユーザー フローまたはポリシーを取得する方法については、「 前提条件」を参照してください。
リソース 範囲 [tasks.read]など、Web API アプリケーションの登録のスコープ。 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 コードが含まれている必要があります。 ブートストラップ 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 からの応答が表示されます。 Bootstrap のカードの 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
    

    .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: webApiScopes プロパティが含まれています (値は配列である必要があります)。これは、Web API で構成され、Web アプリに付与されるスコープです。 また、呼び出される 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 を作成し、ユーザーが資格情報を入力し、アプリケーションに同意できるようにするメソッド。 getAuthCodeUrl メソッドが使用されます。このメソッドは ConfidentialClientApplication クラスで定義されています。

    エクスプレス経路:

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

次のステップ