分享方式:


教學課程:將登入和登出新增至外部租用戶的 Vanilla JavaScript SPA

本教學課程是一系列的最後一個部分,示範如何使用 Microsoft Entra 系統管理中心來建置 Vanilla JS 單頁應用程式 (SPA),並準備進行驗證。 在 本系列的第 3 部分 中,您已在 Visual Studio Code 中建立 Vanilla JS SPA,並將其設定以進行驗證。 最後一個步驟說明如何將登入和登出功能新增至應用程式。

在本教學課程中,您將會;

  • 將程式碼新增至 index.html 檔案,以建立使用者介面
  • 將程式碼新增至 signout.html 檔案,以建立登出頁面
  • 登出與登入應用程式

必要條件

將程式碼新增至 index.html 檔案

SPA 的主頁面 index.html,是啟動應用程式時載入的第一個頁面。 這也是當使用者選取 [登出] 按鈕時所載入的頁面。

  1. 開啟公用/index.html,並新增下列程式碼片段:

     <!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>Microsoft identity platform</title>
         <link rel="SHORTCUT ICON" href="./favicon.svg" type="image/x-icon">
         <link rel="stylesheet" href="./styles.css">
    
         <!-- adding Bootstrap 5 for UI components  -->
         <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet"
             integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
    
         <!-- msal.min.js can be used in the place of msal-browser.js -->
         <script src="/msal-browser.min.js"></script>
     </head>
    
     <body>
         <nav class="navbar navbar-expand-sm navbar-dark bg-primary navbarStyle">
             <a class="navbar-brand" href="/">Microsoft identity platform</a>
             <div class="navbar-collapse justify-content-end">
                 <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>
             </div>
         </nav>
         <br>
         <h5 id="title-div" class="card-header text-center">Vanilla JavaScript single-page application secured with MSAL.js
         </h5>
         <h5 id="welcome-div" class="card-header text-center d-none"></h5>
         <br>
         <div class="table-responsive-ms" id="table">
             <table id="table-div" class="table table-striped d-none">
                 <thead id="table-head-div">
                     <tr>
                         <th>Claim Type</th>
                         <th>Value</th>
                         <th>Description</th>
                     </tr>
                 </thead>
                 <tbody id="table-body-div">
                 </tbody>
             </table>
         </div>
         <!-- importing bootstrap.js and supporting js libraries -->
         <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
             integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous">
             </script>
         <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"
             integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3"
             crossorigin="anonymous"></script>
         <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
             integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
             crossorigin="anonymous"></script>
    
         <!-- importing app scripts (load order is important) -->
         <script type="text/javascript" src="./authConfig.js"></script>
         <script type="text/javascript" src="./ui.js"></script>
         <script type="text/javascript" src="./claimUtils.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="./authPopup.js"></script>
     </body>
    
     </html>
    
  2. 儲存檔案。

將程式碼新增至 claimUtils.js 檔案

  1. 開啟 public/claimUtils.js 並新增下列程式碼片段:

        /**
     * Populate claims table with appropriate description
     * @param {Object} claims ID token claims
     * @returns claimsObject
     */
    const createClaimsTable = (claims) => {
        let claimsObj = {};
        let index = 0;
    
        Object.keys(claims).forEach((key) => {
            if (typeof claims[key] !== 'string' && typeof claims[key] !== 'number') return;
            switch (key) {
                case 'aud':
                    populateClaim(
                        key,
                        claims[key],
                        "Identifies the intended recipient of the token. In ID tokens, the audience is your app's Application ID, assigned to your app in the Azure portal.",
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'iss':
                    populateClaim(
                        key,
                        claims[key],
                        'Identifies the issuer, or authorization server that constructs and returns the token. It also identifies the Azure AD tenant for which the user was authenticated. If the token was issued by the v2.0 endpoint, the URI will end in /v2.0. The GUID that indicates that the user is a consumer user from a Microsoft account is 9188040d-6c67-4c5b-b112-36a304b66dad.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'iat':
                    populateClaim(
                        key,
                        changeDateFormat(claims[key]),
                        'Issued At indicates when the authentication for this token occurred.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'nbf':
                    populateClaim(
                        key,
                        changeDateFormat(claims[key]),
                        'The nbf (not before) claim identifies the time (as UNIX timestamp) before which the JWT must not be accepted for processing.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'exp':
                    populateClaim(
                        key,
                        changeDateFormat(claims[key]),
                        "The exp (expiration time) claim identifies the expiration time (as UNIX timestamp) on or after which the JWT must not be accepted for processing. It's important to note that in certain circumstances, a resource may reject the token before this time. For example, if a change in authentication is required or a token revocation has been detected.",
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'name':
                    populateClaim(
                        key,
                        claims[key],
                        "The principal about which the token asserts information, such as the user of an application. This value is immutable and can't be reassigned or reused. It can be used to perform authorization checks safely, such as when the token is used to access a resource. By default, the subject claim is populated with the object ID of the user in the directory",
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'preferred_username':
                    populateClaim(
                        key,
                        claims[key],
                        'The primary username that represents the user. It could be an email address, phone number, or a generic username without a specified format. Its value is mutable and might change over time. Since it is mutable, this value must not be used to make authorization decisions. It can be used for username hints, however, and in human-readable UI as a username. The profile scope is required in order to receive this claim.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'nonce':
                    populateClaim(
                        key,
                        claims[key],
                        'The nonce matches the parameter included in the original /authorize request to the IDP. If it does not match, your application should reject the token.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'oid':
                    populateClaim(
                        key,
                        claims[key],
                        'The oid (user’s object id) is the only claim that should be used to uniquely identify a user in an Azure AD tenant. The token might have one or more of the following claim, that might seem like a unique identifier, but is not and should not be used as such.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'tid':
                    populateClaim(
                        key,
                        claims[key],
                        'The tenant ID. You will use this claim to ensure that only users from the current Azure AD tenant can access this app.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'upn':
                    populateClaim(
                        key,
                        claims[key],
                        '(user principal name) – might be unique amongst the active set of users in a tenant but tend to get reassigned to new employees as employees leave the organization and others take their place or might change to reflect a personal change like marriage.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'email':
                    populateClaim(
                        key,
                        claims[key],
                        'Email might be unique amongst the active set of users in a tenant but tend to get reassigned to new employees as employees leave the organization and others take their place.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'acct':
                    populateClaim(
                        key,
                        claims[key],
                        'Available as an optional claim, it lets you know what the type of user (homed, guest) is. For example, for an individual’s access to their data you might not care for this claim, but you would use this along with tenant id (tid) to control access to say a company-wide dashboard to just employees (homed users) and not contractors (guest users).',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'sid':
                    populateClaim(key, claims[key], 'Session ID, used for per-session user sign-out.', index, claimsObj);
                    index++;
                    break;
                case 'sub':
                    populateClaim(
                        key,
                        claims[key],
                        'The sub claim is a pairwise identifier - it is unique to a particular application ID. If a single user signs into two different apps using two different client IDs, those apps will receive two different values for the subject claim.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'ver':
                    populateClaim(
                        key,
                        claims[key],
                        'Version of the token issued by the Microsoft identity platform',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'auth_time':
                    populateClaim(
                        key,
                        claims[key],
                        'The time at which a user last entered credentials, represented in epoch time. There is no discrimination between that authentication being a fresh sign-in, a single sign-on (SSO) session, or another sign-in type.',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'at_hash':
                    populateClaim(
                        key,
                        claims[key],
                        'An access token hash included in an ID token only when the token is issued together with an OAuth 2.0 access token. An access token hash can be used to validate the authenticity of an access token',
                        index,
                        claimsObj
                    );
                    index++;
                    break;
                case 'uti':
                case 'rh':
                    index++;
                    break;
                default:
                    populateClaim(key, claims[key], '', index, claimsObj);
                    index++;
            }
        });
    
        return claimsObj;
    };
    
        /**
         * Populates claim, description, and value into an claimsObject
         * @param {string} claim
         * @param {string} value
         * @param {string} description
         * @param {number} index
         * @param {Object} claimsObject
         */
        const populateClaim = (claim, value, description, index, claimsObject) => {
            let claimsArray = [];
            claimsArray[0] = claim;
            claimsArray[1] = value;
            claimsArray[2] = description;
            claimsObject[index] = claimsArray;
        };
    
        /**
         * Transforms Unix timestamp to date and returns a string value of that date
         * @param {string} date Unix timestamp
         * @returns
         */
        const changeDateFormat = (date) => {
            let dateObj = new Date(date * 1000);
            return `${date} - [${dateObj.toString()}]`;
    };
    
  2. 儲存檔案。

將程式碼新增至 signout.html 檔案

  1. 開啟公用/signout.html,並新增下列程式碼片段:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Azure AD | Vanilla JavaScript SPA</title>
        <link rel="SHORTCUT ICON" href="./favicon.svg" type="image/x-icon">
    
        <!-- adding Bootstrap 4 for UI components  -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/boot8strap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    </head>
    <body>
        <div class="jumbotron" style="margin: 10%">
            <h1>Goodbye!</h1>
            <p>You have signed out and your cache has been cleared.</p>
            <a class="btn btn-primary" href="/" role="button">Take me back</a>
        </div>
    </body>
    </html>
    
  2. 儲存檔案。

將程式碼新增至 ui.js 檔案

設定授權時,可以建立使用者介面,以允許使用者在執行專案時登入和登出。 若要建置應用程式的使用者介面 (UI),Bootstrap 用來建立回應式 UI,其中包含登入登出按鈕。

  1. 開啟公用/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');
    
    function welcomeUser(username) {
        signInButton.classList.add('d-none');
        signOutButton.classList.remove('d-none');
        titleDiv.classList.add('d-none');
        welcomeDiv.classList.remove('d-none');
        welcomeDiv.innerHTML = `Welcome ${username}!`;
    };
    
    function updateTable(account) {
        tableDiv.classList.remove('d-none');
    
        const tokenClaims = createClaimsTable(account.idTokenClaims);
    
        Object.keys(tokenClaims).forEach((key) => {
            let row = tableBody.insertRow(0);
            let cell1 = row.insertCell(0);
            let cell2 = row.insertCell(1);
            let cell3 = row.insertCell(2);
            cell1.innerHTML = tokenClaims[key][0];
            cell2.innerHTML = tokenClaims[key][1];
            cell3.innerHTML = tokenClaims[key][2];
        });
    };
    
  2. 儲存檔案。

將程式碼新增至 styles.css 檔案

  1. 開啟公用/styles.css,並新增下列程式碼片段:

    .navbarStyle {
        padding: .5rem 1rem !important;
    }
    
    .table-responsive-ms {
        max-height: 39rem !important;
        padding-left: 10%;
        padding-right: 10%;
    }
    
  2. 儲存檔案。

執行您的專案並登入

現在已經新增所有必要的程式碼片段,可以在網頁瀏覽器中呼叫及測試應用程式。

  1. 開啟新終端機,並執行下列命令來啟動您的快速網頁伺服器。

    npm start
    
  2. 開啟新的私人瀏覽器,並在瀏覽器中輸入應用程式 URI,http://localhost:3000/

  3. 選取 [沒有帳戶?建立帳戶],以啟動註冊流程。

  4. 在 [建立帳戶] 視窗中,輸入向外部租用戶註冊的電子郵件地址,以您的應用程式使用者身分啟動註冊流程。

  5. 從外部租用戶輸入一次性密碼之後,請輸入新的密碼和更多帳戶詳細資料,此註冊流程就會完成。

    1. 如果出現視窗提示您保持登入,請選擇 [是] 或 [否]
  6. SPA 現在將顯示稱為要求設定檔資訊的按鈕。 選取該按鈕以顯示設定檔資料。

    登入 Vanilla JS SPA 的螢幕擷取畫面。

登出應用程式

  1. 若要登出應用程式,請選取導覽列中的 [登出]
  2. 此時會出現一個視窗,詢問要登出哪個帳戶。
  3. 成功登出時,會出現最後一個視窗,建議您關閉所有瀏覽器視窗。

另請參閱