如何將 JavaScript 應用程式從 ADAL.js 遷移至 MSAL.js

適用于 JavaScript 的 Microsoft 驗證程式庫 (MSAL.js,也稱為 msal-browser ) 2.x 是建議在Microsoft 身分識別平臺上搭配 JavaScript 應用程式使用的驗證程式庫。 本文強調您需要進行的變更,以移轉使用 ADAL.js 的應用程式以使用 MSAL.js 2.x

注意

我們強烈建議 MSAL.js 2.x 透過 MSAL.js 1.x。 驗證碼授與流程更安全,並允許單頁應用程式維持良好的使用者體驗,儘管 Safari 等隱私權措施瀏覽器已實作來封鎖協力廠商 Cookie 等優點。

必要條件

  • 您必須在 應用程式註冊入口網站上將 [平臺 / 回復 URL 類型 ] 設定為 [單頁應用程式 ] (如果您已在應用程式註冊中新增其他平臺,例如 Web ,您必須確定重新導向 URI 不會重迭。請參閱: 重新導向 URI 限制
  • 您必須為 MSAL.js 依賴的 ES6 功能提供 polyfills ,才能在 Internet Explorer 上 執行您的應用程式
  • 如果您尚未將 Microsoft Entra 應用程式遷移至 v2 端點

安裝和匯入 MSAL

有兩種方式可以安裝 MSAL.js 2.x 程式庫:

透過 npm:

npm install @azure/msal-browser

然後,根據您的模組系統,匯入它,如下所示:

import * as msal from "@azure/msal-browser"; // ESM

const msal = require('@azure/msal-browser'); // CommonJS

透過 CDN:

在 HTML 檔案的標頭區段中載入腳本:

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"></script>
  </head>
</html>

如需使用 CDN 時的替代 CDN 連結和最佳做法,請參閱: CDN 使用量

初始化 MSAL

在 ADAL.js 中,您會具現化 AuthenticationCoNtext 類別,然後公開可用來達成驗證的方法( loginacquireTokenPopup 等等)。 此物件可作為應用程式與授權伺服器或識別提供者之連線的標記法。 初始化時,唯一的必要參數是 clientId

window.config = {
  clientId: "YOUR_CLIENT_ID"
};

var authContext = new AuthenticationContext(config);

在 MSAL.js 中,您會改為具現化 PublicClientApplication 類別。 如同 ADAL.js,建構函式預期 至少包含 參數的 clientId 組態物件 。 如需詳細資訊,請參閱: 初始化 MSAL.js

const msalConfig = {
  auth: {
      clientId: 'YOUR_CLIENT_ID'
  }
};

const msalInstance = new msal.PublicClientApplication(msalConfig);

在 ADAL.js 和 MSAL.js 中,如果未指定,授權單位 URI 預設為 https://login.microsoftonline.com/common

注意

如果您在 v2.0 中使用 https://login.microsoftonline.com/common 授權單位,將允許使用者使用任何 Microsoft Entra 組織或個人 Microsoft 帳戶 (MSA) 登入。 在 MSAL.js 中,如果您想要限制登入任何 Microsoft Entra 帳戶(與 ADAL.js 的行為相同),請改用 https://login.microsoftonline.com/organizations

設定 MSAL

在 MSAL.js 中初始化 AuthenticationCoNtext 時使用的一些組態選項已被取代,同時引進一些新的設定選項。 請參閱可用選項 的完整清單。 重要的是,除了 clientId 之外,這些選項中的許多都可以在權杖取得期間覆寫,讓您根據每個要求設定這些 選項 。 例如,您可以在取得權杖時,使用不同于 您在初始化期間設定的授權單位 URI 或 重新導向 URI

此外,您不再需要透過組態選項指定登入體驗(也就是使用快顯視窗或重新導向頁面)。 MSAL.js而是透過 PublicClientApplication 實例公開 loginPopuploginRedirect 方法。

啟用 記錄

在 ADAL.js 中,您會在程式碼的任何位置分別設定記錄:

window.config = {
  clientId: "YOUR_CLIENT_ID"
};

var authContext = new AuthenticationContext(config);

var Logging = {
  level: 3,
  log: function (message) {
      console.log(message);
  },
  piiLoggingEnabled: false
};


authContext.log(Logging)

在 MSAL.js 中,記錄是組態選項的一部分,並在 初始化 PublicClientApplication 期間建立:

const msalConfig = {
  auth: {
      // authentication related parameters
  },
  cache: {
      // cache related parameters
  },
  system: {
      loggerOptions: {
          loggerCallback(loglevel, message, containsPii) {
              console.log(message);
          },
          piiLoggingEnabled: false,
          logLevel: msal.LogLevel.Verbose,
      }
  }
}

const msalInstance = new msal.PublicClientApplication(msalConfig);

切換至 MSAL API

ADAL.js 中的某些公用方法在 MSAL.js 中具有對等專案:

ADAL MSAL 備註
acquireToken acquireTokenSilent 已重新命名,現在需要 帳戶 物件
acquireTokenPopup acquireTokenPopup 現在非同步並傳回承諾
acquireTokenRedirect acquireTokenRedirect 現在非同步並傳回承諾
handleWindowCallback handleRedirectPromise 使用重新導向體驗時需要
getCachedUser getAllAccounts 已重新命名,現在會傳回帳戶陣列。

其他方法已被取代,而 MSAL.js 則提供新的方法:

ADAL MSAL 備註
login N/A 已取代。 使用 loginPopuploginRedirect
logOut N/A 已取代。 使用 logoutPopuplogoutRedirect
N/A loginPopup
N/A loginRedirect
N/A logoutPopup
N/A logoutRedirect
N/A getAccountByHomeId 依家庭識別碼篩選帳戶 (oid + 租使用者識別碼)
N/A getAccountLocalId 依本機識別碼篩選帳戶(適用于 ADFS)
N/A getAccountUsername 依使用者名稱篩選帳戶(如果有)

此外,如同在 TypeScript 中實作的 MSAL.js 與 ADAL.js 不同,它會公開您可以在專案中使用的各種類型和介面。 如需詳細資訊, 請參閱 MSAL.js API 參考

使用範圍而非資源

Azure Active Directory v1.0 與 2.0 端點之間的重要差異在於如何存取資源。 搭配 v1.0 端點使用 ADAL.js 時,您會先在應用程式註冊入口網站上註冊許可權,然後要求資源的存取權杖(例如 Microsoft Graph),如下所示:

authContext.acquireTokenRedirect("https://graph.microsoft.com", function (error, token) {
  // do something with the access token
});

MSAL.js 僅 支援 v2.0 端點。 v2.0 端點會 採用以範圍為中心的 模型來存取資源。 因此,當您要求資源的存取權杖時,您也需要指定該資源的範圍:

msalInstance.acquireTokenRedirect({
  scopes: ["https://graph.microsoft.com/User.Read"]
});

以範圍為中心的模型的優點之一是能夠使用 動態範圍 。 使用 v1.0 端點建置應用程式時,您需要註冊應用程式所需的完整許可權集(稱為 靜態範圍 ),讓使用者在登入時同意。 在 v2.0 中,您可以使用 scope 參數在您想要的許可權時要求許可權(因此, 動態範圍 )。 這可讓使用者對範圍提供 累加同意 。 因此,如果一開始您只想讓使用者登入您的應用程式,而且您不需要任何類型的存取權,您可以這麼做。 如果您稍後需要能夠讀取使用者的行事曆,您可以接著在 acquireToken 方法中要求行事曆範圍,並取得使用者的同意。 如需詳細資訊,請參閱: 資源和範圍

使用承諾而不是回呼

在 ADAL.js 中,回呼會在驗證成功且取得回應之後用於任何作業:

authContext.acquireTokenPopup(resource, extraQueryParameter, claims, function (error, token) {
  // do something with the access token
});

在 MSAL.js 中,會改用承諾:

msalInstance.acquireTokenPopup({
      scopes: ["User.Read"] // shorthand for https://graph.microsoft.com/User.Read
  }).then((response) => {
      // do something with the auth response
  }).catch((error) => {
      // handle errors
  });

您也可以使用 ES8 隨附的 async/await 語法:

const getAccessToken = async() => {
  try {
      const authResponse = await msalInstance.acquireTokenPopup({
          scopes: ["User.Read"]
      });
  } catch (error) {
      // handle errors
  }
}

快取和擷取權杖

如同 ADAL.js,MSAL.js 會使用 Web 儲存體 API 快取瀏覽器儲存體中的權杖和其他驗證成品。 建議您使用 sessionStorage 選項(請參閱: 組態 ),因為它在儲存使用者取得的權杖時更安全,但 localStorage 會讓您 跨索引標籤和使用者會話單一登入

重要的是,您不應該直接存取快取。 相反地,您應該使用適當的 MSAL.js API 來擷取存取權杖或使用者帳戶等驗證成品。

使用重新整理權杖更新權杖

ADAL.js 使用 OAuth 2.0 隱含流程 ,因為安全性原因不會傳回重新整理權杖(重新整理權杖的存留期比存取權杖長,因此在惡意執行者手中更為危險)。 因此,ADAL.js 會使用隱藏的 IFrame 執行權杖更新,讓使用者不會重複提示進行驗證。

使用 PKCE 支援的驗證程式代碼流程,使用 MSAL.js 2.x 的應用程式會取得重新整理權杖以及識別碼和存取權杖,可用來更新權杖。 重新整理權杖的使用已抽象化,開發人員不應該建置其周圍的邏輯。 相反地,MSAL 會自行使用重新整理權杖來管理權杖更新。 您先前使用 ADAL.js 的權杖快取無法傳輸至 MSAL.js,因為權杖快取架構已變更,且與 ADAL.js 中使用的架構不相容。

處理錯誤和例外狀況

使用 MSAL.js 時,最常見的錯誤類型是 interaction_in_progress 錯誤。 當另一個互動式 API 仍在進行中時叫用互動式 API 時 loginPopuploginRedirectacquireTokenPopupacquireTokenRedirect 就會擲回此錯誤。 和 acquireToken* API 是 非同步 login* ,因此您必須確保產生的承諾已在叫用另一個承諾之前解決。

另一個常見的錯誤是 interaction_required 。 這個錯誤通常是藉由起始互動式權杖取得提示來解決。 例如,您嘗試存取的 Web API 可能會有 條件式存取 原則,要求使用者執行 多重要素驗證 (MFA)。 在此情況下,藉由觸發 acquireTokenPopupacquireTokenRedirect 會提示使用者輸入 MFA 來處理 interaction_required 錯誤,讓他們能夠完整處理它。

然而,您可能會遇到的另一個常見錯誤是 consent_required ,當使用者未同意取得受保護資源存取權杖所需的許可權時,就會發生這種情況。 如同 在 中 interaction_required ,錯誤的解決方案 consent_required 通常會使用 acquireTokenPopupacquireTokenRedirect 來起始互動式權杖取得提示。

如需詳細資訊,請參閱: 常見的 MSAL.js 錯誤,以及如何處理它們

使用事件 API

MSAL.js ( > =v2.4) 引進事件 API,可讓您在應用程式中使用。 這些事件與驗證程式以及 MSAL 在任何時刻執行的動作相關,而且可用來更新 UI、顯示錯誤訊息、檢查是否有任何互動正在進行等等。 例如,以下是在登入進程因任何原因而失敗時所呼叫的事件回呼:

const callbackId = msalInstance.addEventCallback((message) => {
  // Update UI or interact with EventMessage here
  if (message.eventType === EventType.LOGIN_FAILURE) {
      if (message.error instanceof AuthError) {
          // Do something with the error
      }
    }
});

為了達到效能,當不再需要事件回呼時,請務必取消註冊事件回呼。 如需詳細資訊,請參閱: MSAL.js 事件 API

處理多個帳戶

ADAL.js 具有代表目前已驗證實體的使用者 概念 。 MSAL.js 會將 使用者 取代為 帳戶 ,因為使用者可以有多個帳戶與其相關聯。 這也表示您現在必須控制多個帳戶,並選擇適當的帳戶來處理。 下列程式碼片段說明此程式:

let homeAccountId = null; // Initialize global accountId (can also be localAccountId or username) used for account lookup later, ideally stored in app state

// This callback is passed into `acquireTokenPopup` and `acquireTokenRedirect` to handle the interactive auth response
function handleResponse(resp) {
  if (resp !== null) {
      homeAccountId = resp.account.homeAccountId; // alternatively: resp.account.homeAccountId or resp.account.username
  } else {
      const currentAccounts = myMSALObj.getAllAccounts();
      if (currentAccounts.length < 1) { // No cached accounts
          return;
      } else if (currentAccounts.length > 1) { // Multiple account scenario
          // Add account selection logic here
      } else if (currentAccounts.length === 1) {
          homeAccountId = currentAccounts[0].homeAccountId; // Single account scenario
      }
  }
}

如需詳細資訊,請參閱: MSAL.js 中的帳戶

使用包裝函式程式庫

如果您要針對 Angular 和 React 架構進行開發,則可以分別使用 MSAL Angular v2 MSAL React 。 這些包裝函式會公開與 MSAL.js 相同的公用 API,同時提供架構特定的方法和元件,以簡化驗證和權杖擷取程式。

執行應用程式

變更完成後,請執行應用程式並測試您的驗證案例:

npm start

範例:使用 ADAL.js 保護 SPA 與 MSAL.js

下列程式碼片段示範單頁應用程式使用Microsoft 身分識別平臺驗證使用者所需的最少程式碼,並使用第一個 ADAL.js 和 MSAL.js 取得 Microsoft Graph 的存取權杖:

使用 ADAL.js 使用 MSAL.js

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.0.18/js/adal.min.js"></script>
</head>

<body>
    <div>
        <p id="welcomeMessage" style="visibility: hidden;"></p>
        <button id="loginButton">Login</button>
        <button id="logoutButton" style="visibility: hidden;">Logout</button>
        <button id="tokenButton" style="visibility: hidden;">Get Token</button>
    </div>
    <script>
        // DOM elements to work with
        var welcomeMessage = document.getElementById("welcomeMessage");
        var loginButton = document.getElementById("loginButton");
        var logoutButton = document.getElementById("logoutButton");
        var tokenButton = document.getElementById("tokenButton");

        // if user is logged in, update the UI
        function updateUI(user) {
            if (!user) {
                return;
            }

            welcomeMessage.innerHTML = 'Hello ' + user.profile.upn + '!';
            welcomeMessage.style.visibility = "visible";
            logoutButton.style.visibility = "visible";
            tokenButton.style.visibility = "visible";
            loginButton.style.visibility = "hidden";
        };

        // attach logger configuration to window
        window.Logging = {
            piiLoggingEnabled: false,
            level: 3,
            log: function (message) {
                console.log(message);
            }
        };

        // ADAL configuration
        var adalConfig = {
            instance: 'https://login.microsoftonline.com/',
            clientId: "ENTER_CLIENT_ID_HERE",
            tenant: "ENTER_TENANT_ID_HERE",
            redirectUri: "ENTER_REDIRECT_URI_HERE",
            cacheLocation: "sessionStorage",
            popUp: true,
            callback: function (errorDesc, token, error, tokenType) {
                if (error) {
                    console.log(error, errorDesc);
                } else {
                    updateUI(authContext.getCachedUser());
                }
            }
        };

        // instantiate ADAL client object
        var authContext = new AuthenticationContext(adalConfig);

        // handle redirect response or check for cached user
        if (authContext.isCallback(window.location.hash)) {
            authContext.handleWindowCallback();
        } else {
            updateUI(authContext.getCachedUser());
        }

        // attach event handlers to button clicks
        loginButton.addEventListener('click', function () {
            authContext.login();
        });

        logoutButton.addEventListener('click', function () {
            authContext.logOut();
        });

        tokenButton.addEventListener('click', () => {
            authContext.acquireToken(
                "https://graph.microsoft.com",
                function (errorDesc, token, error) {
                    if (error) {
                        console.log(error, errorDesc);

                        authContext.acquireTokenPopup(
                            "https://graph.microsoft.com",
                            null, // extraQueryParameters
                            null, // claims
                            function (errorDesc, token, error) {
                                if (error) {
                                    console.log(error, errorDesc);
                                } else {
                                    console.log(token);
                                }
                            }
                        );
                    } else {
                        console.log(token);
                    }
                }
            );
        });
    </script>
</body>

</html>


<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.34.0/js/msal-browser.min.js"></script>
</head>

<body>
    <div>
        <p id="welcomeMessage" style="visibility: hidden;"></p>
        <button id="loginButton">Login</button>
        <button id="logoutButton" style="visibility: hidden;">Logout</button>
        <button id="tokenButton" style="visibility: hidden;">Get Token</button>
    </div>
    <script>
        // DOM elements to work with
        const welcomeMessage = document.getElementById("welcomeMessage");
        const loginButton = document.getElementById("loginButton");
        const logoutButton = document.getElementById("logoutButton");
        const tokenButton = document.getElementById("tokenButton");

        // if user is logged in, update the UI
        const updateUI = (account) => {
            if (!account) {
                return;
            }

            welcomeMessage.innerHTML = `Hello ${account.username}!`;
            welcomeMessage.style.visibility = "visible";
            logoutButton.style.visibility = "visible";
            tokenButton.style.visibility = "visible";
            loginButton.style.visibility = "hidden";
        };

        // MSAL configuration
        const msalConfig = {
            auth: {
                clientId: "ENTER_CLIENT_ID_HERE",
                authority: "https://login.microsoftonline.com/ENTER_TENANT_ID_HERE",
                redirectUri: "ENTER_REDIRECT_URI_HERE",
            },
            cache: {
                cacheLocation: "sessionStorage"
            },
            system: {
                loggerOptions: {
                    loggerCallback(loglevel, message, containsPii) {
                        console.log(message);
                    },
                    piiLoggingEnabled: false,
                    logLevel: msal.LogLevel.Verbose,
                }
            }
        };

        // instantiate MSAL client object
        const pca = new msal.PublicClientApplication(msalConfig);

        // handle redirect response or check for cached user
        pca.handleRedirectPromise().then((response) => {
            if (response) {
                pca.setActiveAccount(response.account);
                updateUI(response.account);
            } else {
                const account = pca.getAllAccounts()[0];
                updateUI(account);
            }
        }).catch((error) => {
            console.log(error);
        });

        // attach event handlers to button clicks
        loginButton.addEventListener('click', () => {
            pca.loginPopup().then((response) => {
                pca.setActiveAccount(response.account);
                updateUI(response.account);
            })
        });

        logoutButton.addEventListener('click', () => {
            pca.logoutPopup().then((response) => {
                window.location.reload();
            });
        });

        tokenButton.addEventListener('click', () => {
            const account = pca.getActiveAccount();

            pca.acquireTokenSilent({
                account: account,
                scopes: ["User.Read"]
            }).then((response) => {
                console.log(response);
            }).catch((error) => {
                if (error instanceof msal.InteractionRequiredAuthError) {
                    pca.acquireTokenPopup({
                        scopes: ["User.Read"]
                    }).then((response) => {
                        console.log(response);
                    });
                }

                console.log(error);
            });
        });
    </script>
</body>

</html>

下一步