JavaScript アプリを ADAL.js から MSAL.js に移行する方法

Microsoft Authentication Library for JavaScript (MSAL.js、msal-browser とも呼ばれます) 2.x は、Microsoft ID プラットフォーム上の JavaScript アプリケーションに使用することが推奨される認証ライブラリです。 この記事では、ADAL.js を使用しているアプリを MSAL.js 2.x に移行するために必要な変更について説明します。

注意

MSAL.js 1.x ではなく MSAL.js 2.x を強くお勧めします。 認証コードの付与フローがより安全になり、サードパーティーの Cookie をブロックするために Safari などのブラウザーに実装されているプライバシー対策に関係なく、シングルページ アプリケーションが良好なユーザー エクスペリエンスを維持できるなどの利点があります。

前提条件

  • [アプリの登録] ポータルで [プラットフォーム] / [返信 URL の種類][シングルページ アプリケーション] に設定する必要があります (アプリの登録で、Web など、他のプラットフォームを追加している場合は、リダイレクト URI が重複しないようにする必要があります。詳細は、リダイレクト URI の制限に関するページを参照してください)。
  • Internet Explorer 上でアプリを実行するには、MSAL.js が依存する ES6 の機能 (Promise など) に対して polyfills を指定する必要があります。
  • Microsoft Entra アプリをまだ v2 エンドポイントに移行していない場合は、移行します。

MSAL をインストールしてインポートする

MSAL.js 2.x のライブラリをインストールするには 2 つの方法があります。

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 など) が公開されます。 このオブジェクトは、アプリケーションから承認サーバーや ID プロバイダーへの接続を表すものです。 初期化するときに必須のパラメーターは、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 です。

Note

v2.0 で https://login.microsoftonline.com/common 機関を使用する場合、ユーザーが Microsoft Entra 組織または個人用 Microsoft アカウント (MSA) でサインインできるようにします。 MSAL.js では、Microsoft Entra アカウントへのログインを制限する場合 (ADAL.js の場合と同じ動作)、代わりに https://login.microsoftonline.com/organizations を使用してください。

MSAL を構成する

AuthenticationContext の初期化時に使用される ADAL.js の構成オプションの一部は MSAL.js では非推奨になり、いくつかの新しいオプションが導入されました。 使用できるオプションの詳細な一覧を参照してください。 重要な点は、これらのオプションの多くは、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 非同期になり、Promise が返されるようになりました
acquireTokenRedirect acquireTokenRedirect 非同期になり、Promise が返されるようになりました
handleWindowCallback handleRedirectPromise リダイレクト機能を使用する場合に必要です
getCachedUser getAllAccounts 名前が変更され、アカウントの配列が返されるようになりました。

その他のものは非推奨になりましたが、MSAL.js に新しいメソッドが用意されています。

ADAL MSAL メモ
login 該当なし 非推奨になりました。 loginPopup または loginRedirect を使用します
logOut 該当なし 非推奨になりました。 logoutPopup または logoutRedirect を使用します
該当なし loginPopup
該当なし loginRedirect
該当なし logoutPopup
該当なし logoutRedirect
該当なし getAccountByHomeId ホーム ID (oid + テナント ID) を使用してアカウントをフィルター処理します
該当なし getAccountLocalId ローカル ID を使用してアカウントをフィルター処理します (ADFS に役立ちます)
該当なし getAccountUsername ユーザー名を使用してアカウントをフィルター処理します (存在する場合)

さらに、MSAL.js は ADAL.js とは異なり、TypeScript で実装されているため、プロジェクトで使用できるさまざまな型とインターフェイスが公開されています。 詳細については 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"]
});

スコープ中心モデルの利点の 1 つは、"動的スコープ" を使用できることです。 v1.0 エンドポイントを使用してアプリケーションを作成するときは、ユーザーがログイン時に同意するアプリケーションで必要なアクセス許可 ("静的スコープ" と呼ばれます) の完全なセットを登録する必要がありました。 v2.0 では、スコープ パラメーターを使用して、アクセス許可を必要なときに要求できます (この理由により、"動的スコープ")。 これによって、ユーザーはスコープに増分同意を与えることができます。 最初ユーザーにはアプリケーションへのサインインだけを行わせ、どのような種類のアクセスも必要としない場合、そうすることができます。 その後、ユーザーの予定表を読み取る機能が必要になった場合は、acquireToken メソッドで予定表のスコープを要求してユーザーの同意を得ることができます。 詳細については、「リソースとスコープ」を参照してください

コールバックの代わりに Promise を使用する

ADAL.js では、認証が成功し、応答が取得された後に、すべての操作にコールバックが使用されます。

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

MSAL.js では、Promise が代わりに使用されます。

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 Storage API が使用されます。 ユーザーが取得したトークンをより安全に格納できるため、sessionStorage オプションを使用することが推奨されます (構成に関するページを参照してください)。ただし、localStorage を使用すると、タブとユーザー セッションをまたいだシングル サインオンを実行できるようになります。

重要な点は、キャッシュに直接アクセスすることは想定されていないということです。 その代わり、適切な MSAL.js の API を使用して、アクセス トークンやユーザー アカウントなどの認証成果物を取得する必要があります。

更新トークンを使用してトークンを更新する

ADAL.js には OAuth 2.0 暗黙的フローが使用されています。この場合、セキュリティ上の理由から更新トークンが返されません (更新トークンはアクセス トークンよりも有効期間が長いので、悪意のあるアクターの手に渡るとより危険です)。 そのため、ADAL.js の場合、ユーザーが何度も認証を求められないように、トークンの更新に非表示のフレームが使用されています。

PKCE をサポートする認証コード フローの場合、MSAL.js 2.x を使用するアプリは、ID トークンとアクセス トークンと共に更新トークンを受け取ります。更新にこれを使用することができます。 更新トークンの使用方法は抽象化されており、開発者がそれに関するロジックを構築することは想定されていません。 その代わり、更新トークンを使用したトークンの更新は、MSAL によって自動的に管理されます。 ADAL.js を使用した以前のトークン キャッシュは MSAL.js に転送できません。これは、トークン キャッシュのスキーマが変更され、ADAL.js で使用されているスキーマとは互換性がないためです。

エラーと例外を処理する

MSAL.js を使用している場合に、最も一般的な種類のエラーは、interaction_in_progress エラーです。 このエラーは、ある対話型 API (loginPopuploginRedirectacquireTokenPopupacquireTokenRedirect) が呼び出されたときに、別の対話型 API がまだ進行中だった場合に発生します。 login*acquireToken* の API は "非同期" なので、別の API を呼び出す前に、結果の Promise が解決されていることを確認する必要があります。

もう 1 つの一般的なエラーは interaction_required です。 多くの場合、このエラーは対話型トークン取得のプロンプトを開始するだけで解決されます。 たとえば、アクセスしようとしている Web API に条件付きアクセス ポリシーが設定されている場合があり、その場合、ユーザーは多要素認証 (MFA) を実行する必要があります。 この場合、acquireTokenPopup または acquireTokenRedirect をトリガーして interaction_required エラーを処理すると、ユーザーに MFA を要求するプロンプトが表示され、それを実行できるようになります。

発生する可能性のあるもう一つの一般的なエラーは consent_required です。これは、保護されたリソースのアクセス トークンを取得するために必要な権限がユーザーによって同意されていない場合に発生します。 interaction_required の場合と同様に、consent_required エラーのソリューションによって、acquireTokenPopup または acquireTokenRedirect を使用した対話的なトークン取得のプロンプトが開始されることがよくあります。

詳細については、一般的な 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 v2MSAL React を使用できます。 これらのラッパーにより、MSAL.js と同じパブリック API が公開されています。さらに、フレームワーク固有のメソッドやコンポーネントが用意されているので、認証とトークンの取得プロセスを効率化することができます。

アプリを実行する

変更が完了したら、アプリを実行し、認証シナリオをテストします。

npm start

例: ADAL.js と MSAL.js の対比を使用した SPA のセキュリティ保護

次のスニペットは、ADAL.js と MSAL.js を使用して、Microsoft ID プラットフォームでユーザーを認証し、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>

次のステップ