处理 MSAL.NET 中的错误和异常

本文概述不同类型的错误,并提供处理常见登录错误的相关建议。

MSAL 错误处理基础知识

Microsoft 身份验证库 (MSAL) 中的异常旨在帮助应用开发者进行故障排除,而不会向最终用户显示。 异常消息未经本地化。

处理异常和错误时,可以使用异常类型本身和错误代码来区分不同的异常。 有关错误代码的列表,请参阅 Microsoft Entra 身份验证和授权错误代码

在登录体验期间,可能会遇到有关许可、条件访问(MFA、设备管理、基于位置的限制)、令牌颁发和兑换以及用户属性的错误。

以下部分提供了有关应用错误处理的更多详细信息。

MSAL.NET 中的错误处理

异常类型

当库本身检测到错误状态(例如配置错误)时,将引发 MsalClientException

当标识提供者(Microsoft Entra ID)返回错误响应时,将引发 MsalServiceException。 这是对服务器错误信息的翻译。

MsalUIRequiredExceptionMsalServiceException 的类型,指示需要用户交互。 例如,当需要多重身份验证(MFA)或用户更改其密码且无法以无提示方式获取令牌时。

处理异常

处理.NET异常时,可以使用异常类型本身和ErrorCode成员来区分异常。 ErrorCode 值是 MsalError 类型的常量。

还可以查看 MsalClientExceptionMsalServiceExceptionMsalUIRequiredException 的字段。

如果抛出了 MsalServiceException,请参阅 身份验证和授权错误代码,查看其中是否列出了该代码。

如果抛出 MsalUIRequiredException,则表明需要进行交互式流程,以便用户解决该问题。 在桌面和移动应用等公共客户端应用中,通过调用 AcquireTokenInteractive来解决此问题,它显示浏览器。 在机密客户端应用中,Web 应用应将用户重定向到授权页,Web API 应返回 HTTP 状态代码和标头,指示身份验证失败(401 未授权和 WWW-Authenticate 标头)。

常见.NET异常

下面是可能引发的常见异常和一些可能的缓解措施:

Exception 错误代码 缓解措施
MsalUiRequiredException AADSTS65001:用户或管理员尚未同意使用 ID 为“{appId}”、名为“{appName}”的应用程序。 针对此用户和资源发送交互式授权请求。 首先获取用户同意。 如果您不使用 .NET Core(它本身不包含任何 Web UI),请调用 AcquireTokenInteractive(且仅调用一次)。 如果你使用的是 .NET Core,或者不想执行 AcquireTokenInteractive,用户可以转到某个 URL 以给予同意:https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&scope=user.read 调用 AcquireTokenInteractiveapp.AcquireTokenInteractive(scopes).WithAccount(account).WithClaims(ex.Claims).ExecuteAsync();
MsalUiRequiredException AADSTS50079:用户需要使用多重身份验证(MFA)。 没有缓解措施。 如果你的租户配置了 MFA 且 Microsoft Entra ID 决定强制执行,则回退到交互式流,例如 AcquireTokenInteractive
MsalServiceException AADSTS90010:/common/consumers 端点不支持此授权类型。 请使用 /organizations 或特定于租户的终结点。 你使用了 /common 正如 Microsoft Entra ID 消息中解释的那样,颁发机构需要有租户,否则为 /organizations
MsalServiceException AADSTS70002:请求正文必须包含以下参数: client_secret or client_assertion 如果应用程序未在 Microsoft Entra ID 中注册为公共客户端应用程序,则可能会引发此异常。 在 Microsoft Entra 管理中心中,编辑应用程序的清单,并将 allowPublicClient 设置为 true
MsalClientException unknown_user Message:无法识别已登录用户 库无法查询当前已登录的 Windows 用户,或者该用户未加入 Active Directory 或 Microsoft Entra(不支持工作场所加入的用户)。 缓解措施:实现您自己的逻辑来获取用户名(例如,john@contoso.com),并使用接受用户名作为参数的 AcquireTokenByIntegratedWindowsAuth 形式。
MsalClientException 托管用户不支持集成 Windows 身份验证 此方法依赖于由 Active Directory (AD) 公开的协议。 如果在没有 AD 支持(“托管”用户)的Microsoft Entra ID中创建用户,此方法将失败。 在 AD 中创建并由Microsoft Entra ID(“联合”用户)提供支持的用户可以从这种非交互式身份验证方法中受益。 缓解:使用交互式身份验证。

MsalUiRequiredException

调用AcquireTokenSilent()时,MSAL.NET 返回的常见状态代码之一是MsalError.InvalidGrantError。 此状态代码表示应用程序应再次调用身份验证库,但要在交互模式下调用(用于公共客户端应用程序的 AcquireTokenInteractive 或 AcquireTokenByDeviceCodeFlow 会在 Web 应用中执行质询)。 这是因为在颁发身份验证令牌之前需要其他用户交互。

大多数情况下 AcquireTokenSilent 失败时,这是因为令牌缓存没有与请求匹配的令牌。 访问令牌在 1 小时内过期,并 AcquireTokenSilent 尝试基于刷新令牌提取新令牌(在 OAuth2 术语中,这是“刷新令牌”流)。 此流也可能因各种原因而失败,例如,如果租户管理员配置更严格的登录策略。

交互旨在让用户执行操作。 其中一些条件便于用户解决(例如,单击一下即可接受使用条款),有些条件无法通过当前配置解决(例如,相关计算机需要连接到特定的公司网络)。 有些帮助用户设置多重身份验证,或在其设备上安装Microsoft Authenticator。

MsalUiRequiredException 分类枚举

MSAL 公开一个 Classification 字段,你可以阅读该字段以提供更好的用户体验。 例如,若要告知用户其密码已过期,或者他们需要提供许可才能使用某些资源。 支持的值是 UiRequiredExceptionClassification 枚举的一部分:

Classification Meaning 建议的处理
BasicAction 在交互式身份验证流程中,此情况可通过用户交互来解决。 调用 AcquireTokenInteractively()。
附加操作 该情况可在交互式身份验证流程之外,通过与系统进行额外的补救性交互来解决。 调用 AcquireTokenInteractively()以显示解释修正操作的消息。 如果用户不太可能完成修正操作,调用应用程序可能会选择隐藏需要additional_action的流。
MessageOnly 目前无法解决此问题。 启动交互式身份验证流将显示说明条件的消息。 调用 AcquireTokenInteractively()以显示说明条件的消息。 AcquireTokenInteractively() 将在用户读取消息并关闭窗口后返回 UserCanceled 错误。 如果用户不太可能从该消息中获益,则调用应用程序可能会选择隐藏导致 message_only 的流。
需要同意 用户同意缺失或已被撤销。 调用 AcquireTokenInteractively(),以便用户同意。
用户密码已过期 用户的密码已过期。 调用 AcquireTokenInteractively(),以便用户可以重置其密码。
PromptNeverFailed 通过参数 prompt=never 调用交互式身份验证,强制 MSAL 依赖浏览器 cookie,而不是显示浏览器。 此操作失败。 在没有 Prompt.None 的情况下调用 AcquireTokenInteractively()
AcquireTokenSilentFailed MSAL SDK 没有足够的信息从缓存中提取令牌。 这是因为缓存中未找到令牌或找不到帐户。 错误消息具有更多详细信息。 调用 AcquireTokenInteractively()。
没有 未提供进一步的详细信息。 该情况可能会在交互式身份验证流程中通过用户交互得到解决。 调用 AcquireTokenInteractively()。

.NET代码示例

AuthenticationResult res;
try
{
 res = await application.AcquireTokenSilent(scopes, account)
        .ExecuteAsync();
}
catch (MsalUiRequiredException ex) when (ex.ErrorCode == MsalError.InvalidGrantError)
{
 switch (ex.Classification)
 {
  case UiRequiredExceptionClassification.None:
   break;
  case UiRequiredExceptionClassification.MessageOnly:
  // You might want to call AcquireTokenInteractive(). Azure AD will show a message
  // that explains the condition. AcquireTokenInteractively() will return UserCanceled error
  // after the user reads the message and closes the window. The calling application may choose
  // to hide features or data that result in message_only if the user is unlikely to benefit 
  // from the message
  try
  {
      res = await application.AcquireTokenInteractive(scopes).ExecuteAsync();
  }
  catch (MsalClientException ex2) when (ex2.ErrorCode == MsalError.AuthenticationCanceledError)
  {
   // Do nothing. The user has seen the message
  }
  break;

  case UiRequiredExceptionClassification.BasicAction:
  // Call AcquireTokenInteractive() so that the user can, for instance accept terms
  // and conditions

  case UiRequiredExceptionClassification.AdditionalAction:
  // You might want to call AcquireTokenInteractive() to show a message that explains the remedial action. 
  // The calling application may choose to hide flows that require additional_action if the user 
  // is unlikely to complete the remedial action (even if this means a degraded experience)

  case UiRequiredExceptionClassification.ConsentRequired:
  // Call AcquireTokenInteractive() for user to give consent.
  
  case UiRequiredExceptionClassification.UserPasswordExpired:
  // Call AcquireTokenInteractive() so that user can reset their password
  
  case UiRequiredExceptionClassification.PromptNeverFailed:
  // You used WithPrompt(Prompt.Never) and this failed
  
  case UiRequiredExceptionClassification.AcquireTokenSilentFailed:
  default:
  // May be resolved by user interaction during the interactive authentication flow.
  res = await application.AcquireTokenInteractive(scopes)
                         .ExecuteAsync(); break;
 }
}

条件访问和声明质询

以静默方式获取令牌时,如果你尝试访问的 API 需要条件访问声明质询(例如 MFA 策略),则应用程序可能会收到错误。

处理此错误的模式是使用 MSAL 以交互方式获取令牌。 这会提示用户,并使他们能够满足所需的条件访问策略。

在某些情况下调用需要条件访问的 API 时,API 返回的错误中可能会包含声明质询。 例如,如果条件访问策略要求使用托管设备 (Intune),则错误将类似于 AADSTS53000:需要管理你的设备才能访问此资源。 在这种情况下,可以在获取令牌调用中传递声明,以提示用户满足相应的策略要求。

当从 MSAL.NET 调用需要条件访问的 API 时,你的应用需要处理声明质询异常。 这表现为 MsalServiceException,其中 Claims 属性不为空。

若要处理声明质询,请使用 WithClaims(String)

出现错误和异常后重试

调用 MSAL 时,应实现自己的重试策略。 MSAL 会对 Microsoft Entra 服务进行 HTTP 调用,有时会失败。 例如网络崩溃或服务器重载。

HTTP 429

如果服务令牌服务器 (STS) 因请求过多而重载,则将返回 HTTP 错误 429,并在 Retry-After 响应字段中提示还要多久才能重试。

HTTP 错误代码 500-600

MSAL.NET 为 HTTP 错误代码 500-600 的错误实现简单的重试一次机制。

MsalServiceException 以属性System.Net.Http.Headers.HttpResponseHeaders的形式浮出水面namedHeaders。 可以使用错误代码中的其他信息来提高应用程序的可靠性。 在上述情况下,可以使用 RetryAfter 属性(类型为 RetryConditionHeaderValue),并计算何时重试。

下面是使用客户端凭据流的守护程序应用程序的示例。 你可以将其调整为适用于获取令牌的任一方法。


bool retry = false;
do
{
    TimeSpan? delay;
    try
    {
         result = await publicClientApplication.AcquireTokenForClient(scopes, account).ExecuteAsync();
    }
    catch (MsalServiceException serviceException)
    {
         if (serviceException.ErrorCode == "temporarily_unavailable")
         {
             RetryConditionHeaderValue retryAfter = serviceException.Headers.RetryAfter;
             if (retryAfter.Delta.HasValue)
             {
                 delay = retryAfter.Delta;
             }
             else if (retryAfter.Date.HasValue)
             {
                 delay = (retryAfter.Date.Value – DateTimeOffset.Now).TotalMilliseconds;
             }
         }
    }
    // . . .
    if (delay.HasValue)
    {
        Thread.Sleep((int)delay.Value.TotalMilliseconds); // sleep or other
        retry = true;
    }
} while (retry);

后续步骤

请考虑启用 MSAL.NET 中的日志记录,以帮助你诊断和调试问题。