共用方式為


使用 Azure AD B2C 在您自己的單頁應用程式中啟用驗證

本文說明如何將 Azure Active Directory B2C (Azure AD B2C) 驗證新增至您自己的單頁應用程式 (SPA)。 了解如何使用適用於 JavaScript 的 Microsoft 驗證程式庫 (MSAL.js) 來建立 SPA 應用程式。

使用本文章搭配在範例 SPA 應用程式中設定驗證,將範例 SPA 應用程式取代為您自己的 SPA 應用程式。

概觀

本文使用 Node.js 和 Express 來建立基本 Node.js Web 應用程式。 Express 是最小且具有彈性的 Node.js Web 應用程式架構,可為 Web 和行動應用程式提供一組功能。

MSAL.js 驗證程式庫是 Microsoft 提供的程式庫,可簡化將驗證和授權支援新增至 SPA 應用程式的過程。

提示

整個 MSAL.js 程式碼會在用戶端上執行。 您可以使用其他解決方案 (例如 .NET Core、JAVA 和超文字前置處理器 (PHP) 指令碼語言) 來取代 Node.js 和 Express 伺服器端程式碼。

必要條件

若要檢閱必要條件和整合指示,請參閱在範例 SPA 應用程式中設定驗證

步驟 1:建立 SPA 應用程式專案

您可以使用現有的 SPA 應用程式專案或建立新專案。 若要建立新專案,請執行下列動作:

  1. 開啟命令殼層,然後建立新的目錄 (例如,myApp)。 此目錄將包含您的應用程式程式碼、使用者介面和組態檔。

  2. 輸入您所建立的目錄。

  3. 使用 npm init 命令為您的應用程式建立 package.json 檔案。 此命令會提示您提供應用程式的相關資訊 (例如,應用程式的名稱和版本,以及初始進入點的名稱,index.js 檔案)。 執行下列命令,並接受預設值:

npm init

步驟 2:安裝相依性

若要安裝 Express 套件,請在命令殼層中執行下列命令:

npm install express

為了找出應用程式的靜態檔案,伺服器端程式碼會使用 Path 套件。

若要安裝 Path 套件,請在命令殼層中執行下列命令:

npm install path

步驟 3:設定 Web 伺服器

myApp 資料夾中,建立名為 index.js 的檔案,其中包含下列程式碼:

// Initialize express
const express = require('express');
const app = express();

// The port to listen to incoming HTTP requests
const port = 6420;

// Initialize path
const path = require('path');

// Set the front-end folder to serve public assets.
app.use(express.static('App'));

// Set up a route for the index.html
app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname + '/index.html'));
});

// Start the server, and listen for HTTP requests
app.listen(port, () => {
  console.log(`Listening on http://localhost:${port}`);
});

步驟 4:建立 SPA 使用者介面

新增 SPA 應用程式 index.html 檔案。 此檔案會實作以啟動程序架構建置的使用者介面,並針對組態、驗證和 Web API 呼叫匯入指令檔。

下表詳細說明 index.html 檔案所參考的資源:

參考 定義
MSAL.js 程式庫 MSAL.js 驗證 JavaScript 程式庫 CDN 路徑
啟動程序樣式表 免費的前端架構,可讓您更快速且更輕鬆地進行 Web 開發。 此架構包含以 HTML 為基礎和以 CSS 為基礎的設計範本。
policies.js 包含 Azure AD B2C 自訂原則和使用者流程。
authConfig.js 包含驗證組態參數。
authRedirect.js 包含驗證邏輯。
apiConfig.js 包含 Web API 範圍和 API 端點位置。
api.js 定義用來呼叫 API 並處理其回應的方法。
ui.js 控制 UI 元素。

若要轉譯 SPA 索引檔案,請在 myApp 資料夾中建立名為 index.html的檔案,其中包含下列 HTML 程式碼片段:

<!DOCTYPE html>
<html>
    <head>
        <title>My Azure AD B2C test app</title>
    </head>
    <body>
        <h2>My Azure AD B2C test app</h2>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
        <button type="button" id="signIn" class="btn btn-secondary" onclick="signIn()">Sign-in</button>
        <button type="button" id="signOut" class="btn btn-success d-none" onclick="signOut()">Sign-out</button>
        <h5 id="welcome-div" class="card-header text-center d-none"></h5>
        <br />
        <!-- Content -->
        <div class="card">
            <div class="card-body text-center">
                <pre id="response" class="card-text"></pre>
                <button type="button" id="callApiButton" class="btn btn-primary d-none" onclick="passTokenToApi()">Call API</button>
            </div>
        </div>
        <script src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js" integrity="sha384-ggh+EF1aSqm+Y4yvv2n17KpurNcZTeYtUZUvhPziElsstmIEubyEB6AIVpKLuZgr" crossorigin="anonymous"></script>

        <!-- Importing app scripts (load order is important) -->
        <script type="text/javascript" src="./apiConfig.js"></script>
        <script type="text/javascript" src="./policies.js"></script>
        <script type="text/javascript" src="./authConfig.js"></script>
        <script type="text/javascript" src="./ui.js"></script>

        <!-- <script type="text/javascript" src="./authRedirect.js"></script>   -->
        <!-- uncomment the above line and comment the line below if you would like to use the redirect flow -->
        <script type="text/javascript" src="./authRedirect.js"></script>
        <script type="text/javascript" src="./api.js"></script>
    </body>
</html>

步驟 5:設定驗證程式庫

設定 MSAL.js 程式庫如何與 Azure AD B2C 整合。 MSAL.js 程式庫會使用一般組態物件來連線到您 Azure AD B2C 租用戶的驗證端點。

若要設定驗證程式庫,請執行下列動作:

  1. myApp 資料夾中,建立名為 App 的新資料夾。

  2. App 資料夾中,建立名為 authConfig.js 的新檔案。

  3. 將下列 JavaScript 程式碼新增至 authConfig.js 檔案:

    const msalConfig = {
        auth: {
        clientId: "<Application-ID>", 
        authority: b2cPolicies.authorities.signUpSignIn.authority, 
        knownAuthorities: [b2cPolicies.authorityDomain], 
        redirectUri: "http://localhost:6420",
        },
        cache: {
        cacheLocation: "localStorage", .
        storeAuthStateInCookie: false, 
        }
    };
    
    const loginRequest = {
    scopes: ["openid", ...apiConfig.b2cScopes],
    };
    
    const tokenRequest = {
    scopes: [...apiConfig.b2cScopes],
    forceRefresh: false
    };
    
  4. <Application-ID> 取代為您的應用程式註冊應用程式識別碼。 如需詳細資訊,請參閱在範例 SPA 應用程式中設定驗證

提示

如需 MSAL 物件組態選項的詳細資訊,請參閱驗證選項一文。

步驟 6:指定您的 Azure AD B2C 使用者流程

建立 policies.js 檔案,提供 Azure AD B2C 環境的相關資訊。 MSAL.js 程式庫會使用此資訊來建立 Azure AD B2C 的驗證要求。

若要指定您的 Azure AD B2C 使用者流程,請執行下列動作:

  1. App 資料夾中,建立名為 policies.js 的新檔案。

  2. 將下列程式碼新增至 policies.js 檔案:

    const b2cPolicies = {
        names: {
            signUpSignIn: "B2C_1_SUSI",
            editProfile: "B2C_1_EditProfile"
        },
        authorities: {
            signUpSignIn: {
                authority: "https://contoso.b2clogin.com/contoso.onmicrosoft.com/Your-B2C-SignInOrSignUp-Policy-Id",
            },
            editProfile: {
                authority: "https://contoso.b2clogin.com/contoso.onmicrosoft.com/Your-B2C-EditProfile-Policy-Id"
            }
        },
        authorityDomain: "contoso.b2clogin.com"
    }
    
  3. B2C_1_SUSI 取代為您的登入 Azure AD B2C 原則名稱。

  4. B2C_1_EditProfile 取代為您的編輯設定檔 Azure AD B2C 原則名稱。

  5. contoso 的所有執行個體取代為您的 Azure AD B2C 租用戶名稱

步驟 7:使用 MSAL 來登入使用者

在此步驟中,請實作方法來初始化登入流程、API 存取權杖取得和登出方法。

如需詳細資訊,請參閱 使用 Microsoft 驗證程式庫 (MSAL) 登入使用者 一文。

若要登入使用者,請執行下列動作:

  1. App 資料夾中,建立名為 authRedirect.js 的新檔案。

  2. 在您的 authRedirect.js 中,複製並貼上下列程式碼:

    // Create the main myMSALObj instance
    // configuration parameters are located at authConfig.js
    const myMSALObj = new msal.PublicClientApplication(msalConfig);
    
    let accountId = "";
    let idTokenObject = "";
    let accessToken = null;
    
    myMSALObj.handleRedirectPromise()
        .then(response => {
            if (response) {
                /**
                 * For the purpose of setting an active account for UI update, we want to consider only the auth response resulting
                 * from SUSI flow. "tfp" claim in the id token tells us the policy (NOTE: legacy policies may use "acr" instead of "tfp").
                 * To learn more about B2C tokens, visit https://learn.microsoft.com/azure/active-directory-b2c/tokens-overview
                 */
                if (response.idTokenClaims['tfp'].toUpperCase() === b2cPolicies.names.signUpSignIn.toUpperCase()) {
                    handleResponse(response);
                }
            }
        })
        .catch(error => {
            console.log(error);
        });
    
    
    function setAccount(account) {
        accountId = account.homeAccountId;
        idTokenObject = account.idTokenClaims;
        myClaims= JSON.stringify(idTokenObject);
        welcomeUser(myClaims);
    }
    
    function selectAccount() {
    
        /**
         * See here for more information on account retrieval: 
         * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
         */
    
        const currentAccounts = myMSALObj.getAllAccounts();
    
        if (currentAccounts.length < 1) {
            return;
        } else if (currentAccounts.length > 1) {
    
            /**
             * Due to the way MSAL caches account objects, the auth response from initiating a user-flow
             * is cached as a new account, which results in more than one account in the cache. Here we make
             * sure we are selecting the account with homeAccountId that contains the sign-up/sign-in user-flow, 
             * as this is the default flow the user initially signed-in with.
             */
            const accounts = currentAccounts.filter(account =>
                account.homeAccountId.toUpperCase().includes(b2cPolicies.names.signUpSignIn.toUpperCase())
                &&
                account.idTokenClaims.iss.toUpperCase().includes(b2cPolicies.authorityDomain.toUpperCase())
                &&
                account.idTokenClaims.aud === msalConfig.auth.clientId 
                );
    
            if (accounts.length > 1) {
                // localAccountId identifies the entity for which the token asserts information.
                if (accounts.every(account => account.localAccountId === accounts[0].localAccountId)) {
                    // All accounts belong to the same user
                    setAccount(accounts[0]);
                } else {
                    // Multiple users detected. Logout all to be safe.
                    signOut();
                };
            } else if (accounts.length === 1) {
                setAccount(accounts[0]);
            }
    
        } else if (currentAccounts.length === 1) {
            setAccount(currentAccounts[0]);
        }
    }
    
    // in case of page refresh
    selectAccount();
    
    async function handleResponse(response) {
    
        if (response !== null) {
            setAccount(response.account);
        } else {
            selectAccount();
        }
    }
    
    function signIn() {
        myMSALObj.loginRedirect(loginRequest);
    }
    
    function signOut() {
        const logoutRequest = {
            postLogoutRedirectUri: msalConfig.auth.redirectUri,
        };
    
        myMSALObj.logoutRedirect(logoutRequest);
    }
    
    function getTokenRedirect(request) {
        request.account = myMSALObj.getAccountByHomeId(accountId); 
    
        return myMSALObj.acquireTokenSilent(request)
            .then((response) => {
                // In case the response from B2C server has an empty accessToken field
                // throw an error to initiate token acquisition
                if (!response.accessToken || response.accessToken === "") {
                    throw new msal.InteractionRequiredAuthError;
                } else {
                    console.log("access_token acquired at: " + new Date().toString());
                    accessToken = response.accessToken;
                    passTokenToApi();
                }
            }).catch(error => {
                console.log("Silent token acquisition fails. Acquiring token using popup. \n", error);
                if (error instanceof msal.InteractionRequiredAuthError) {
                    // fallback to interaction when silent call fails
                    return myMSALObj.acquireTokenRedirect(request);
                } else {
                    console.log(error);   
                }
        });
    }
    
    // Acquires and access token and then passes it to the API call
    function passTokenToApi() {
        if (!accessToken) {
            getTokenRedirect(tokenRequest);
        } else {
            try {
                callApi(apiConfig.webApi, accessToken);
            } catch(error) {
                console.log(error); 
            }
        }
    }
    
    function editProfile() {
    
    
        const editProfileRequest = b2cPolicies.authorities.editProfile;
        editProfileRequest.loginHint = myMSALObj.getAccountByHomeId(accountId).username;
    
        myMSALObj.loginRedirect(editProfileRequest);
    }
    

步驟 8:設定 Web API 位置和範圍

若要允許您的 SPA 應用程式呼叫 Web API,請提供 Web API 端點位置,以及用來授權存取 Web API 的範圍

若要設定 Web API 位置和範圍,請執行下列動作:

  1. App 資料夾中,建立名為 apiConfig.js 的新檔案。

  2. 在您的 apiConfig.js 中,複製並貼上下列程式碼:

    // The current application coordinates were pre-registered in a B2C tenant.
    const apiConfig = {
        b2cScopes: ["https://contoso.onmicrosoft.com/tasks/tasks.read"],
        webApi: "https://mydomain.azurewebsites.net/tasks"
    };
    
  3. 使用您的租用戶名稱取代 contoso。 您可以依照設定範圍文章中所述,找到必要的範圍名稱。

  4. webApi 的值取代為您的 Web API 端點位置。

步驟 9:呼叫您的 Web API

定義 API 端點的 HTTP 要求。 HTTP 要求設定為將使用 MSAL.js 取得的存取權杖傳遞至要求中的 Authorization HTTP 標頭。

下列程式碼會定義 API 端點的 HTTP GET 要求,並在 Authorization HTTP 標頭中傳遞存取權杖。 API 位置是由 apiConfig.js 中的 webApi 索引鍵所定義。

若要使用您取得的權杖來呼叫 Web API,請執行下列動作:

  1. App 資料夾中,建立名為 api.js 的新檔案。

  2. 將下列程式碼新增至 api.js 檔案:

    function callApi(endpoint, token) {
    
        const headers = new Headers();
        const bearer = `Bearer ${token}`;
    
        headers.append("Authorization", bearer);
    
        const options = {
            method: "GET",
            headers: headers
        };
    
        logMessage('Calling web API...');
    
        fetch(endpoint, options)
        .then(response => response.json())
        .then(response => {
    
            if (response) {
            logMessage('Web API responded: ' + response.name);
            }
    
            return response;
        }).catch(error => {
            console.error(error);
        });
    }
    

步驟 10:新增 UI 元素參考

SPA 應用程式使用 JavaScript 來控制 UI 元素。 例如,它會顯示登入和登出按鈕,並將使用者的識別碼權杖宣告轉譯至畫面。

若要新增 UI 元素參考,請執行下列動作:

  1. App 資料夾中,建立名為 ui.js 的檔案。

  2. 將下列程式碼新增至 ui.js 檔案:

    // Select DOM elements to work with
    const signInButton = document.getElementById('signIn');
    const signOutButton = document.getElementById('signOut')
    const titleDiv = document.getElementById('title-div');
    const welcomeDiv = document.getElementById('welcome-div');
    const tableDiv = document.getElementById('table-div');
    const tableBody = document.getElementById('table-body-div');
    const editProfileButton = document.getElementById('editProfileButton');
    const callApiButton = document.getElementById('callApiButton');
    const response = document.getElementById("response");
    const label = document.getElementById('label');
    
    function welcomeUser(claims) {
        welcomeDiv.innerHTML = `Token claims: </br></br> ${claims}!`
    
        signInButton.classList.add('d-none');
        signOutButton.classList.remove('d-none');
        welcomeDiv.classList.remove('d-none');
        callApiButton.classList.remove('d-none');
    }
    
    function logMessage(s) {
        response.appendChild(document.createTextNode('\n' + s + '\n'));
    }
    

步驟 11:執行 SPA 應用程式

在命令殼層中,執行下列命令:

npm install  
npm ./index.js
  1. 移至 https://localhost:6420.
  2. 選取 [登入]。
  3. 完成註冊或登入程序。

成功驗證之後,已剖析的識別碼權杖會顯示在畫面上。 選取 Call API 以呼叫您的 API 端點。

後續步驟