リスクとポリシーはリアルタイムで評価されるため、一部のリソース API のトークンの有効期間は最大で 28 時間延長される可能性があります。 このような有効期間が長いトークンは、Microsoft Authentication Library (MSAL) によって事前に更新されるため、アプリケーションの回復性が向上します。
CAE を使用するには、アプリもそれがアクセスするリソース API も CAE 対応であることが必要です。 リソース API によって CAE が実装されていて、CAE を処理できることがご利用のアプリケーションで宣言されている場合、アプリは、そのリソースに対する CAE トークンを受信します。 このため、アプリが CAE 対応であることを宣言する場合、アプリケーションは、Microsoft Identity アクセス トークンを受け入れるすべてのリソース API の CAE 要求チャレンジを処理する必要があります。
ただし、CAE 対応リソースをサポートするようにコードを準備しても、CAE をサポートしていない API を操作する機能が制限されるわけではありません。 アプリが CAE 応答を正しく処理しない場合、技術的には有効でも CAE が原因で取り消されるトークンを使用する API 呼び出しをアプリが繰り返し再試行する可能性があります。
まずは、CAE が原因で呼び出しを拒否しているリソース API からの応答を処理するコードを追加します。 CAE では、アクセス トークンが取り消されているとき、または API が使用されている IP アドレスの変更を検出すると、API は 401 状態と WWW-Authenticate
ヘッダーを返します。 この WWW-Authenticate
ヘッダーには、アプリケーションが新しいアクセス トークンを取得するために使用できる要求チャレンジが含まれています。
これらの条件が満たされると、アプリは MSAL.NET WwwAuthenticateParameters
クラスを使用して、要求チャレンジを抽出してデコードすることができます。
if (APIresponse.IsSuccessStatusCode)
{
// ...
}
else
{
if (APIresponse.StatusCode == System.Net.HttpStatusCode.Unauthorized
&& APIresponse.Headers.WwwAuthenticate.Any())
{
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(APIresponse.Headers);
アプリは次に、要求チャレンジを使用して、リソースの新しいアクセス トークンを取得します。
try
{
authResult = await _clientApp.AcquireTokenSilent(scopes, firstAccount)
.WithClaims(claimChallenge)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (MsalUiRequiredException)
{
try
{
authResult = await _clientApp.AcquireTokenInteractive(scopes)
.WithClaims(claimChallenge)
.WithAccount(firstAccount)
.ExecuteAsync()
.ConfigureAwait(false);
}
// ...
アプリケーションで CAE 対応リソースから返された要求チャレンジを処理する準備ができたら、アプリが CAE 対応であることを Microsoft Identity に伝えることができます。 これを MSAL アプリケーション内で行うには、"cp1"
というクライアント機能を使用してパブリック クライアントを構築します。
_clientApp = PublicClientApplicationBuilder.Create(App.ClientId)
.WithDefaultRedirectUri()
.WithAuthority(authority)
.WithClientCapabilities(new [] {"cp1"})
.Build();
これらの条件が満たされると、アプリは API 応答ヘッダーから次のように要求チャレンジを抽出できます。
try {
const response = await fetch(apiEndpoint, options);
if (response.status === 401 && response.headers.get('www-authenticate')) {
const authenticateHeader = response.headers.get('www-authenticate');
const claimsChallenge = parseChallenges(authenticateHeader).claims;
// use the claims challenge to acquire a new access token...
}
} catch(error) {
// ...
}
// helper function to parse the www-authenticate header
function parseChallenges(header) {
const schemeSeparator = header.indexOf(' ');
const challenges = header.substring(schemeSeparator + 1).split(',');
const challengeMap = {};
challenges.forEach((challenge) => {
const [key, value] = challenge.split('=');
challengeMap[key.trim()] = window.decodeURI(value.replace(/['"]+/g, ''));
});
return challengeMap;
}
アプリは次に、要求チャレンジを使用して、リソースの新しいアクセス トークンを取得します。
const tokenRequest = {
claims: window.atob(claimsChallenge), // decode the base64 string
scopes: ['User.Read'],
account: msalInstance.getActiveAccount()
};
let tokenResponse;
try {
tokenResponse = await msalInstance.acquireTokenSilent(tokenRequest);
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
tokenResponse = await msalInstance.acquireTokenPopup(tokenRequest);
}
}
アプリケーションで CAE 対応のリソースから返された要求チャレンジを処理する準備ができたら、MSAL 構成に clientCapabilities
プロパティを追加することで、そのアプリが CAE に対応可能な状態にあることを Microsoft Identity に伝えることができます。
const msalConfig = {
auth: {
clientId: 'Enter_the_Application_Id_Here',
clientCapabilities: ["CP1"]
// remaining settings...
}
}
const msalInstance = new PublicClientApplication(msalConfig);
これらの条件が満たされると、アプリは API 応答ヘッダーから次のように要求チャレンジを抽出できます。
import msal # pip install msal
import requests # pip install requests
import www_authenticate # pip install www-authenticate==0.9.2
# Once your application is ready to handle the claim challenge returned by a CAE-enabled resource, you can tell Microsoft Identity your app is CAE-ready. To do this in your MSAL application, build your Public Client using the Client Capabilities of "cp1".
app = msal.PublicClientApplication("your_client_id", client_capabilities=["cp1"])
...
# When these conditions are met, the app can extract the claims challenge from the API response header as follows:
response = requests.get("<your_resource_uri_here>")
if response.status_code == 401 and response.headers.get('WWW-Authenticate'):
parsed = www_authenticate.parse(response.headers['WWW-Authenticate'])
claims = parsed.get("bearer", {}).get("claims")
# Your app would then use the claims challenge to acquire a new access token for the resource.
if claims:
auth_result = app.acquire_token_interactive(["scope"], claims_challenge=claims)
CP1 クライアント機能のサポートを宣言する
アプリケーション構成内では、CP1
クライアント機能を含めることで、アプリケーションが CAE をサポートすることを宣言する必要があります。 これは、client_capabilities
JSON プロパティを使用することで指定します。
{
"client_id" : "<your_client_id>",
"authorization_user_agent" : "DEFAULT",
"redirect_uri" : "msauth://<pkg>/<cert_hash>",
"multiple_clouds_supported":true,
"broker_redirect_uri_registered": true,
"account_mode": "MULTIPLE",
"client_capabilities": "CP1",
"authorities" : [
{
"type": "AAD",
"audience": {
"type": "AzureADandPersonalMicrosoftAccount"
}
}
]
}
実行時に CAE チャレンジに対応する
リソースに対して要求を行い、その応答に要求チャレンジが含まれている場合は、次の要求で使用するために、それを抽出し、MSAL に送り返します。
final HttpURLConnection connection = ...;
final int responseCode = connection.getResponseCode();
// Check the response code...
if (200 == responseCode) {
// ...
} else if (401 == responseCode) {
final String authHeader = connection.getHeaderField("WWW-Authenticate");
if (null != authHeader) {
final ClaimsRequest claimsRequest = WWWAuthenticateHeader
.getClaimsRequestFromWWWAuthenticateHeaderValue(authHeader);
// Feed the challenge back into MSAL, first silently, then interactively if required
final AcquireTokenSilentParameters silentParameters = new AcquireTokenSilentParameters.Builder()
.fromAuthority(authority)
.forAccount(account)
.withScopes(scope)
.withClaims(claimsRequest)
.build();
try {
final IAuthenticationResult silentRequestResult = mPublicClientApplication.acquireTokenSilent(silentParameters);
// If successful - your business logic goes here...
} catch (final Exception e) {
if (e instanceof MsalUiRequiredException) {
// Retry the request interactively, passing in any claims challenge...
}
}
}
} else {
// ...
}
// Don't forget to close your connection
以下のコード スニペットでは、トークンをサイレントで取得し、リソース プロバイダーに対する HTTP 呼び出しを行った後、CAE ケースを処理するフローについて説明します。 サイレント呼び出しが要求で失敗した場合は、追加の対話呼び出しが必要になる場合があります。
CP1 クライアント機能のサポートを宣言する
アプリケーション構成内では、CP1
クライアント機能を含めることで、アプリケーションが CAE をサポートすることを宣言する必要があります。 これは、clientCapabilities
プロパティを使用することで指定します。
let clientConfigurations = MSALPublicClientApplicationConfig(clientId: "contoso-app-ABCDE-12345",
redirectUri: "msauth.com.contoso.appbundle://auth",
authority: try MSALAuthority(url: URL(string: "https://login.microsoftonline.com/organizations")!))
clientConfigurations.clientApplicationCapabilities = ["CP1"]
let applicationContext = try MSALPublicClientApplication(configuration: clientConfigurations)
要求チャレンジを解析するためのヘルパー関数を実装します。
func parsewwwAuthenticateHeader(headers:Dictionary<AnyHashable, Any>) -> String? {
// !! This is a sample code and is not validated, please provide your own implementation or fully test the sample code provided here.
// Can also refer here for our internal implementation: https://github.com/AzureAD/microsoft-authentication-library-common-for-objc/blob/dev/IdentityCore/src/webview/embeddedWebview/challangeHandlers/MSIDPKeyAuthHandler.m#L112
guard let wwwAuthenticateHeader = headers["WWW-Authenticate"] as? String else {
// did not find the header, handle gracefully
return nil
}
var parameters = [String: String]()
// regex mapping
let regex = try! NSRegularExpression(pattern: #"(\w+)="([^"]*)""#)
let matches = regex.matches(in: wwwAuthenticateHeader, range: NSRange(wwwAuthenticateHeader.startIndex..., in: wwwAuthenticateHeader))
for match in matches {
if let keyRange = Range(match.range(at: 1), in: wwwAuthenticateHeader),
let valueRange = Range(match.range(at: 2), in: wwwAuthenticateHeader) {
let key = String(wwwAuthenticateHeader[keyRange])
let value = String(wwwAuthenticateHeader[valueRange])
parameters[key] = value
}
}
guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) else {
// cannot convert params into json date, end gracefully
return nil
}
return String(data: jsonData, encoding: .utf8)
}
401 / 要求チャレンジをキャッチして解析します。
let response = .... // HTTPURLResponse object from 401'd service response
switch response.statusCode {
case 200:
// ...succeeded!
break
case 401:
let headers = response.allHeaderFields
// Parse header fields
guard let wwwAuthenticateHeaderString = self.parsewwwAuthenticateHeader(headers: headers) else {
// 3.7 no valid wwwAuthenticateHeaderString is returned from header, end gracefully
return
}
let claimsRequest = MSALClaimsRequest(jsonString: wwwAuthenticateHeaderString, error: nil)
// Create claims request
let parameters = MSALSilentTokenParameters(scopes: "Enter_the_Protected_API_Scopes_Here", account: account)
parameters.claimsRequest = claimsRequest
// Acquire token silently again with the claims challenge
applicationContext.acquireTokenSilent(with: parameters) { (result, error) in
if let error = error {
// error happened end flow gracefully, and handle error. (e.g. interaction required)
return
}
guard let result = result else {
// no result end flow gracefully
return
}
// Success - You got a token!
}
break
default:
break
}
これらの条件が満たされると、アプリは API 応答ヘッダーから次のように要求チャレンジを抽出できます。
クライアント機能をアドバタイズします。
client, err := New("client-id", WithAuthority(authority), WithClientCapabilities([]string{"cp1"}))
WWW-Authenticate
ヘッダーを解析し、結果のチャレンジを MSAL-Go に渡します。
// No snippet provided at this time
要求チャレンジを使用してサイレントでのトークンの取得を試みます。
var ar AuthResult;
ar, err := client.AcquireTokenSilent(ctx, tokenScope, public.WithClaims(claims))
ユーザーをサインインさせてから、Azure portal を使用してユーザーのセッションを取り消すことで、アプリケーションをテストできます。 CAE 対応の API が次にアプリで呼び出されたとき、ユーザーは再認証を行うように求められます。