MSAL.js v2,v3による認証認可
MSAL.js v2,v3は「Authorization Code Flow(認可コードフロー)+PKCE」フローのみをサポートしていますが、「Authorization Code Flow(認可コードフロー)+PKCE」フローと実際のMSAL.jsを使用した認証認可処理が結びつかず不明確な箇所について、調査した結果をまとめます。
前提として以下のフレームワーク・サービス・プロトコルを想定して調査を行いました。
用語 | 前提フレームワーク・サービス・プロトコル |
---|---|
IdP | Azure EntraID |
ログイン認証プロトコル | OpenID Connect |
WebAPI認可プロトコル | OAuth2.0 |
アーキテクチャ | REST API+SPA |
SPA | Angular.js |
WebAPI | ASP.NET Core WebAPI |
PKCEの部分の説明は省略します。PKCEの部分の説明は以下URLに詳しく記載されています。
認証認可シーケンス図
こちらのマイクロソフトのサイトにあるSPAのシーケンス図はOAuth2.0の「Authorization Code Flow(認可コードフロー)+PKCE」フローになりますが、OpenID Connectの場合もシーケンス図は同じになります。異なるのはトークンエンドポイントから返されるトークンにIDトークンが追加されるくらいです。
また以下に記載されているようにSPA+EntraID+MSAL.js v2,v3では認証プロトコルにOpenID Connectが自動で使用され、非推奨のImplicit Flow(インプリシットフロー)ではなく、Authorization Code Flow(認可コードフロー)を使用する場合、PKCEが自動的に使用されます。つまりcode_challengeやcode_challenge_methodパラメータが自動的に付与されます。
Microsoft ID プラットフォームでは、認証に OpenID Connect プロトコルを使用し、OAuth 2.0 で定義されている 2 種類の承認付与のいずれかを使用するという方法でこれらのアプリがサポートされます。 SPA を開発するときに、認証コード フローを PKCE (Proof Key for Code Exchange) と一緒に使用します。
1. EntraIDのログイン画面でログイン認証
EntraID(IdP)のログイン画面で、ユーザー/パスワードを入力して、認可エンドポイントとのログイン認証を行います。
SPA(Angular.js)
2. 認可コードの取得およびリダイレクト
ログイン認証後、Authorization Code Flow(認可コードフロー)であるため認可エンドポイントから、短命の認可コードがEntraID(IdP)からユーザーに返されますが、認可コードが付与されている場合、MSAL.jsは、コールバックであると自動で検知して、フロントエンド(RP)のURLへのリダイレクト(リダイレクトURI)までやってくれます。
MSAL.jsは初期化時に、AzureADからのコールバックでフロントエンドが呼ばれた場合の、クエリー文字列にOAuthの認証コードが付与されている場合には、「コールバックが来たぞ!」と検知して、OpenID Connectの認証処理の続きを行ってくれます。そして、その後redirectStartPageで指定したURLにリダイレクトまでやってくれます。そのために、init()で待ち(正確にはauth.handleRedirectPromise()を待つ)を入れています。
3. リダイレクト後のSPA画面起動
SPA画面起動時にMSALのClientを作成します。その際、cache(キャッシュ)プロパティでトークン保存先のブラウザストレージの保存場所を指定します。
MsalModule.forRoot(
new PublicClientApplication({
// MSAL Configuration
auth: {
clientId: "clientid",
authority: "https://login.microsoftonline.com/common/",
redirectUri: "http://localhost:4200/",
postLogoutRedirectUri: "http://localhost:4200/",
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: true, // set to true for IE 11
},
system: {
loggerOptions: {
loggerCallback: () => {},
piiLoggingEnabled: false,
},
},
}
)
4. リダイレクト後の処理
リダイレクト後に実装する処理(5.と6.と7.の処理)は、handleRedirectCallback,handleRedirectPromise内に実装します。
handleRedirectCallback,handleRedirectPromise自体は、画面初期処理(Angularの場合、ngOnInit())に実装します。以下はhandleRedirectCallbackの例
ngOnInit(): void {
let errorMessage = '';
let retryTimes = 0;
this.authService.handleRedirectCallback((error, response) => {
if (error) {
console.error(error)
} else {
console.error(error)
// 何らかの画面初期処理時のWebAPI呼出し処理
this.getUnitInfo();
}
});
5. アクセストークン取得
リダイレクト後、データ取得のためWebAPIをコールして、その際WebAPIの認可を行うためにトークンエンドポイントからアクセストークンを取得するのですが、以下のように記載されている文章が多いです。
フロントエンド(RP)は、受け取った「認可コード」をパラメータとしてトークンエンドにトークンをリクエストします。
EntraID(IdP)はレスポンスとして「アクセストークン」「IDトークン」を返します。(※ リフレッシュトークンはオプション)
https://qiita.com/nabeatsu/items/380058915629c0ce795e#トークンリクエスト
MSAL.jsにて具体的にWebAPIの認可で必要なアクセストークンを取得する方法は、acquireTokenSilentにてサイレントに取得します。リダイレクト後の画面初期処理時にWebAPIをコールしたい場合はhandleRedirectCallback内の処理として記述します。
acquireTokenSilentの引数でスコープ「openid」を指定することで、アクセストークンに加えてIDトークンも取得することが可能です。
const requestObj = {
scopes: ['user.read']
};
this.authService.acquireTokenSilent(requestObj).then((tokenResponse) => {
sessionStorage.setItem('accessToken', tokenResponse.accessToken);
}
}).catch((error) => {
alert(error);
});
}
acquireTokenSilentはログイン状態のみ使用可能です。未ログイン状態でacquireTokenSilentを使用した場合、例外エラーとなります。
acquireTokenSilentのアクセストークンを取得する順番は以下の通りです。
① 3.のcache(キャッシュ)で指定したブラウザストレージ(SessionStorage/LocalStorage/Cookies)(これをキャッシュと呼んでいる)に有効なアクセストークンがある場合、取得する。
② ①にて有効なアクセストークンが無い場合、更新トークンを使用して新しいアクセストークンをトークンエンドポイントから取得する。
また以下のように書かれていますので、MSAL.jsを使用した場合のデフォルトのトークンの有効期限は24時間ということになります。(ただしトークンの有効期限の24時間が切れてもログイン画面にリダイレクトされるわけでは無いようです。アクティブなセッションって何だ?)
トークンの有効期限が切れるまでは①のブラウザストレージ(キャッシュ)を使うのが基本的な流れです。
更新トークンの24時間の有効期間も期限切れになった場合、MSAL.jsは非表示のiframeを開き、Microsoft EntraIDとの既存のアクティブなセッション (存在する場合) を利用して新しい認可コードをサイレントに要求します。その後、これは新しいトークンセット (アクセスおよび更新トークン) と交換されます。
https://learn.microsoft.com/ja-jp/entra/identity-platform/scenario-spa-acquire-token?tabs=javascript2
このようにアクセストークンはEntraIDでセッション管理(有効期限管理)・保持されるため、
「SessionIDを使ったセッション管理」(DBでSessionIDを管理・保持)とは異なり、DBによるアクセストークンの管理・保持が不要となります。
6. アクセストークン保存
5.で取得したアクセストークンをブラウザストレージ(SessionStorage/LocalStorage/Cookies)に保存します。
※ 3.のcache(キャッシュ)を指定している場合、この6.自体不要かもしれません。
sessionStorage.setItem('accessToken', tokenResponse.accessToken);
7. WebAPIをコール
取得したアクセストークンをリクエストヘッダの「Authorization: Bearer」に付与してWebAPIをコールします。
REST API(ASP.NET Core WebAPI)
8. アクセストークン検証チェック設定
REST API(ASP.NET Core WebAPI)にて行うアクセストークンの検証チェック設定(検証自体ではなく、どのような検証チェックを行うかの事前設定)を行います。
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
このメソッドの内部では、JwtBearerOptionsを設定し、アクセストークンの検証チェック設定を構成します。上記のようにJwtBearerOptionsを特に指定しなければ、以下のデフォルト値で内部的にアクセストークン検証が行われます。
・Authority: https://login.microsoftonline.com/{TenantId}/v2.0
・Audience(ValidAudience): api://{ClientId}
・TokenValidationParameters の設定
- ValidateIssuer: true(トークン発行者をチェック)
- ValidateAudience: true(APIの audクレームをチェック)
- ValidateLifetime: true(トークンの有効期限をチェック)
- ValidateIssuerSigningKey: true(トークンの署名を検証)
AddMicrosoftIdentityWebApiの内部のアクセストークンの検証チェック設定は、以下の辺りに記述されています。
この設定により、受信したアクセストークンがEntraIDによって正しく発行されたトークンか、有効期限内のトークンかなどの検証が行われます。
ちなみにAddMicrosoftIdentityWebApiと同じアクセストークンの検証チェック設定をする拡張メソッドとしてAddJwtBearerがあります。以下の違い、使い分けがされるようですが、AddMicrosoftldentityWebApiも最終的に内部処理でAddJwtBearerを呼び出しています。
メソッド名 | メソッド用途 |
---|---|
AddMicrosoftIdentityWebApi | EntraIDに特化したEntraIDを使った認証を簡単に設定できる拡張メソッド。Microsoft.Identity.Webに含まれている。 |
AddJwtBearer | 汎用的なJWT認証を設定するためのメソッド。EntraID以外のIdP(Auth0/Firebase/カスタムIdPなど)に対応している。 |
9. アクセストークン検証チェック
リクエストがREST API(ASP.NET Core WebAPI)に到達すると、ASP.NET Coreの認証ミドルウェアがJwtBearerHandlerを呼び出し、8.の検証チェック設定を元に以下の順番でアクセストークンを検証します。
① AuthorizationのBearerからアクセストークンを取得する。
② JwtBearerHandlerがJwtSecurityTokenHandlerを使ってアクセストークンを解析する。
TokenValidationParametersに基づき、以下を検証します。
- 署名の検証: EntraIDの公開鍵を使ってsig(署名が改ざんされていないかチェック
- 発行者(Issuer)の検証: issクレームがhttps://login.microsoftonline.com/{TenantId}/v2.0 であるかチェック
- 受信者(Audience)の検証: audクレームがapi://{ClientId}と一致するかチェック
- 有効期限の検証: exp(有効期限)やnbf(開始時間)が正しいかチェック
③ 検証に成功すれば、HttpContext.UserにClaimsPrincipalをセット(認証成功)して、検証に失敗すると、401 Unauthorized を返します。
調査まとめ
-
SPA+EntraID+MSAL.jsでは認証プロトコルにOpenID Connectが自動で使用され、非推奨のImplicit Flow(インプリシットフロー)ではなく、Authorization Code Flow(認可コードフロー)を使用する場合、PKCEが自動的に使用される。
-
この前提においては、MSAL.js v2,v3(「Authorization Code Flow(認可コードフロー)+PKCE」フロー)ではIDトークンの検証は行われない。ログイン認証には認可コードを使用して、IDトークンは使用しない。
ここで、認可コードフローの場合はTLSで保護されたチャネル経由で直接IDトークンが発行されるため、TLSサーバー証明書の検証さえ行っていればIDトークンの改ざんリスクは無視できます。そのためIDトークン自体の署名検証はOPTIONAL(任意)とされているようです。
https://qiita.com/nabeatsu/items/380058915629c0ce795e#idトークン検証
- Authorization Code Flow(認可コードフロー)はそもそもトークンはクライアント(RP)とIdPのみでやりとりし、ブラウザに渡さないフロー(代わりに認可コードを使用するフロー)だが、SPAの場合は普通にブラウザにトークンを返して、ブラウザストレージ(SessionStorage/LocalStorage/Cookies)に保存する。
ここで「何で認可コードが返ってくるんだろう? 最終的にほしいのはトークンなのになー。」と思った方、さすがでございます。
Idpからリダイレクトで戻ってくるということは、「IdP → ユーザー → RP」とユーザーを経由して戻ってくるということです。
もう少し具体的に言うと、IdPからユーザーへのレスポンスに遷移先としてRPのURLが設定されていて、認可コードはそのリクエストパラメーターとして設定されています。
ユーザー側を経由する方式のため、何らかの不具合やセキュリティ的な攻撃により、漏洩するリスクがあります。
https://qiita.com/nabeatsu/items/380058915629c0ce795e#認可コードリダイレクト
- IDトークンはブラウザストレージに保存しない。アクセストークンはブラウザストレージに有効期限内まで保存される。
- ASP.NET Core WebAPIではアクセストークン検証は自動でやってくれる。
参考情報
こちらのMSAL.jsはv1
Discussion