Microsoft 标识平台中的访问令牌

访问令牌是一种为授权而设计的安全令牌,代表经过身份验证的用户授予对特定资源的访问权限。 访问令牌中的信息确定用户是否有权访问特定资源,类似于解锁建筑物中特定门的钥匙。 构成令牌的这些单独信息片段称为声明。 因此,它们是敏感的凭据,如果处理不当,会带来安全风险。 访问令牌不同于 ID 令牌,后者用作身份验证的证明。

访问令牌使客户端可以安全地调用受保护的 Web API, 虽然客户端应用程序可接收和使用访问令牌,但它们应被视为不透明字符串。 客户端应用程序不应尝试验证访问令牌。 在接受访问令牌作为授权证明之前,资源服务器应对其进行验证。 令牌的内容仅用于 API,这意味着访问令牌必须被视为不透明的字符串。 开发人员可以使用 jwt.ms 之类的站点来解码 JWT,但仅限进行验证和调试。 Microsoft API 收到的令牌可能并不总是可以被解码的 JWT。

客户端应使用随访问令牌一起返回的令牌响应数据,了解其中内容的详细信息。 当客户端请求访问令牌时,Microsoft 标识平台还会返回一些有关访问令牌的元数据供应用程序使用。 此信息包含访问令牌的过期时间及其有效范围。 此数据可让应用程序执行访问令牌的智能缓存,而无需分析访问令牌本身。 本文介绍有关访问令牌的基本信息,包括格式、所有权、生存期以及 API 如何验证和使用访问令牌中的声明。

注意

除非另行说明,否则本页上的所有文档仅适用于为已注册 API 颁发的令牌。 它不适用于为 Microsoft 拥有的 API 颁发的令牌,不能使用这些令牌来验证 Microsoft 标识平台如何为已注册 API 颁发令牌。

令牌格式

Microsoft 标识平台提供了两个版本的访问令牌:v1.0 和 v2.0。 这些版本控制令牌中有哪些声明,确保 Web API 可以控制其令牌的样式。

Web API 在注册过程中选择了其中一个版本作为默认值:

  • v1.0,适用于仅 Microsoft Entra 应用程序。

  • v2.0,用于支持使用者帐户的应用程序。

可以通过在应用部件清单中向 accessTokenAcceptedVersion 设置提供适当的值来为应用程序设置版本。 其中的 null1 值会生成 v1.0 令牌,2 会生成 v2.0 令牌。

令牌所有权

访问令牌请求涉及两个参与方:请求令牌的客户端、接受令牌的资源 (Web API)。 令牌中的 aud 声明定义了令牌适用的资源(其受众)。 客户端使用令牌,但不应理解或尝试分析它。 资源接受令牌。

Microsoft 标识平台支持从任何版本的终结点颁发任何令牌版本。 例如,当 accessTokenAcceptedVersion 的值为 2 时,调用 v1.0 终结点以获取该资源的令牌的客户端会收到 v2.0 访问令牌。

资源始终使用aud 声明拥有其令牌,并且是唯一可以更改其令牌详细信息的应用程序。

令牌生存期

访问令牌的默认生存期是可变的。 颁发后,Microsoft 标识平台会分配一个随机值,范围介于 60-90 分钟之间(平均 75 分钟),作为访问令牌的默认生存期。 该变体通过在一段时间内分散访问令牌需求来提高服务复原能力,从而防止 Microsoft Entra ID 的流量每小时出现峰值。

对于 Microsoft Teams 和 Microsoft 365 等客户端,不使用条件访问的租户的默认访问令牌生存期为两小时。

调整访问令牌的生存期,控制客户端应用程序使应用程序会话过期的频率,以及它要求用户重新进行身份验证(以无提示方式或交互方式)的频率。 若要替代默认的访问令牌生存期变化,请使用可配置的令牌生存期 (CTL)

向启用了连续访问评估 (CAE) 的组织应用默认令牌生存期变化。 即使组织使用 CTL 策略,也应用默认令牌生存期变化。 长期令牌生存期的默认令牌生存期范围为 20 到 28 小时。 访问令牌过期后,客户端必须使用刷新令牌以无提示方式获取新的刷新令牌和访问令牌。

组织如果使用条件访问登录频率 (SIF) 来执行登录发生的频率,则不能替代默认访问令牌生存期变体。 组织使用 SIF 时,客户端凭据提示之间的时间是令牌生存期(范围为 60 - 90 分钟)加上登录频率间隔。

下面举例说明了默认令牌生命周期变体如何与登录频率配合使用。 假设组织将登录频率设置为每小时一次。 由于令牌生存期的变化,当令牌的生存期范围为 60-90 分钟时,实际的登录间隔发生在 1 小时到 2.5 小时之间的任意位置。

如果具有生存期为 1 小时的令牌的用户在 59 分钟执行交互式登录,则不会出现凭据提示,因为登录低于 SIF 阈值。 如果新令牌的生存期为 90 分钟,则用户将在一个半小时内看不到凭据提示。 在无提示续订尝试期间,Microsoft Entra ID 需要凭据提示,因为总会话长度已超过 1 小时的登录频率设置。 在此示例中,由于 SIF 间隔和令牌生存期变体导致的凭据提示之间的时间差将为 2.5 小时。

验证令牌

并非所有应用程序都应当验证令牌。 只有在特定的场景中,应用程序才应当验证令牌:

  • Web API 必须验证由客户端发送给它们的访问令牌。 它们必须仅接受包含其 AppId URI 之一作为 aud 声明的令牌。
  • Web 应用必须使用混合流中的用户浏览器验证发送给它们的 ID 令牌,然后才允许访问用户的数据或建立会话。

如果上述方案均不适用,则无需验证令牌。 验证 ID 令牌对本机、桌面或单页应用程序之类的公共客户端没有好处,因为应用程序直接与 IDP 通信,其中 SSL 保护确保 ID 令牌有效。 它们不应验证访问令牌,因为这些令牌供 Web API 验证,而不是供客户端进行验证。

API 和 Web 应用程序只能验证 aud 声明与应用程序匹配的令牌。 其他资源可能具有自定义令牌验证规则。 例如,根据这些规则,无法验证 Microsoft Graph 的令牌,因为它们采用了专有格式。 验证和接受用于另一个资源的令牌是混淆代理问题的一个示例。

如果应用程序需要验证 ID 令牌或访问令牌,则应首先根据 OpenID 发现文档中的值来验证令牌和颁发者的签名。

Microsoft Entra 中间件具有用于验证访问令牌的内置功能,请参阅示例以找出一个以相应语言编写的示例。 还有多个第三方开放源代码库可用于 JWT 验证。 有关身份验证库和代码示例的详细信息,请参阅身份验证库。 如果 Web 应用或 Web API 在 ASP.NET 或 ASP.NET Core 上,请使用 Microsoft.Identity.Web 来处理验证。

v1.0 和 v2.0 令牌

  • 当 Web 应用/API 验证 v1.0 令牌(ver 声明 = 1.0)时,它需要从 v1.0 终结点读取 OpenID Connect 元数据文档 (https://login.partner.microsoftonline.cn/{example-tenant-id}/.well-known/openid-configuration),即使为 Web 应用配置的颁发机构是 v2.0 颁发机构也是如此。
  • 当 Web 应用/API 验证 v2.0 令牌(ver 声明 = 2.0)时,它需要从 v2.0 终结点读取 OpenID Connect 元数据文档 (https://login.partner.microsoftonline.cn/{example-tenant-id}/v2.0/.well-known/openid-configuration),即使为 Web 应用配置的颁发机构是 v1.0 颁发机构也是如此。

以下示例假定你的应用程序正在验证 v2.0 访问令牌(因此,引用 v2.0 版 OIDC 元数据文档和密钥)。 如果验证 v1.0 令牌,只需移除 URL 中的“/v2.0”即可。

验证颁发者

OpenID Connect Core 显示“颁发者标识符 [...] 必须与颁发者声明的值完全匹配。”对于使用特定于租户的元数据终结点的应用程序(例如 https://login.partner.microsoftonline.cn/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/.well-known/openid-configurationhttps://login.partner.microsoftonline.cn/contoso.partner.onmschina.cn/v2.0/.well-known/openid-configuration),只需满足此条件即可。

Microsoft Entra ID 中提供了 https://login.partner.microsoftonline.cn/common/v2.0/.well-known/openid-configuration 与租户无关的文档版本。 此终结点将返回颁发者值 https://login.partner.microsoftonline.cn/{tenantid}/v2.0。 应用程序可以使用此独立于租户租户的终结点,通过以下修改来验证每个租户的令牌:

  1. 应用程序不应期望令牌中的颁发者声明与元数据中的颁发者值完全匹配,而是应将颁发者元数据中的 {tenantid} 值替换为作为当前请求的目标的租户 ID,然后检查是否完全匹配。

  2. 应用程序应使用从密钥终结点返回的 issuer 属性来限制密钥的范围。

    • 具有类似 https://login.partner.microsoftonline.cn/{tenantid}/v2.0 的颁发者值的密钥可以与任何匹配的令牌颁发者一起使用。
    • 具有类似 https://login.partner.microsoftonline.cn/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 的颁发者值的密钥只能与完全匹配一起使用。

    Microsoft Entra 独立于租户的密钥终结点 (https://login.partner.microsoftonline.cn/common/discovery/v2.0/keys) 将返回如下文档:

    {
      "keys":[
        {"kty":"RSA","use":"sig","kid":"A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u","x5t":"A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u","n":"spv...","e":"AQAB","x5c":["MIID..."],"issuer":"https://login.partner.microsoftonline.cn/{tenantid}/v2.0"},
        {"kty":"RSA","use":"sig","kid":"C2dE3fH4iJ5kL6mN7oP8qR9sT0uV1w","x5t":"C2dE3fH4iJ5kL6mN7oP8qR9sT0uV1w","n":"wEM...","e":"AQAB","x5c":["MIID..."],"issuer":"https://login.partner.microsoftonline.cn/{tenantid}/v2.0"},
        {"kty":"RSA","use":"sig","kid":"E3fH4iJ5kL6mN7oP8qR9sT0uV1wX2y","x5t":"E3fH4iJ5kL6mN7oP8qR9sT0uV1wX2y","n":"rv0...","e":"AQAB","x5c":["MIID..."],"issuer":"https://login.partner.microsoftonline.cn/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"}
      ]
    }
    
  3. 使用 Microsoft Entra 租户 ID (tid) 声明而不是标准颁发者声明作为信任边界的应用程序应确保租户 ID 声明是一个 GUID,并且颁发者和租户 ID 相匹配。

对于接受来自多个租户的令牌的应用程序,使用独立于租户的元数据会更高效。

注意

使用独立于 Microsoft Entra 租户的元数据时,声明应在租户中解释,就像在标准 OpenID Connect 下,声明在颁发者中解释一样。 也就是说,{"sub":"ABC123","iss":"https://login.partner.microsoftonline.cn/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0","tid":"aaaabbbb-0000-cccc-1111-dddd2222eeee"}{"sub":"ABC123","iss":"https://login.partner.microsoftonline.cn/bbbbcccc-1111-dddd-2222-eeee3333ffff/v2.0","tid":"bbbbcccc-1111-dddd-2222-eeee3333ffff"} 描述不同的用户,即使 sub 是相同的,因为像 sub 这样的声明是在颁发者/租户的上下文中解释的。

验证签名

JWT 包含三段,以 . 字符分隔。 第一个段称为标头,第二个称为主体,第三个称为签名。 使用签名段来评估令牌的真实性。

Microsoft Entra ID 颁发使用行业标准非对称加密算法(如 RS256)签名的令牌。 JWT 的标头包含用于签名令牌的密钥和加密方法的相关信息:

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "H4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3a",
  "kid": "H4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3a"
}

alg 声明表示用于对令牌进行签名的算法,而 kid 声明表示用于验证令牌的特定公钥。

在任何给定时间点,Microsoft Entra ID 可以使用一组特定公钥-私钥对中的一个对来签名 ID 令牌。 Microsoft Entra ID 定期轮换一组可能的密钥,因此请编写应用程序以自动处理这些密钥更改。 检查 Microsoft Entra ID 所用公钥的更新的合理频率大约为每 24 小时一次。

可使用位于以下位置的 OpenID Connect 元数据文档来获取验证签名所需的签名密钥数据:

https://login.partner.microsoftonline.cn/common/v2.0/.well-known/openid-configuration

提示

在浏览器中尝试此操作:URL

以下信息介绍了元数据文档:

  • 是一个 JSON 对象,其中包含一些有用的信息,例如执行 OpenID Connect 身份验证所需的各种终结点的位置。
  • 包含 jwks_uri,提供公钥集的位置,这些公钥对应于那些用于对令牌进行签名的私钥。 位于 jwks_uri 的 JSON Web 密钥 (JWK) 包含在该特定时间点使用的所有公钥信息。 RFC 7517 描述了 JWK 格式。 应用程序可以使用 JWT 标头中的 kid 声明从本文档中选择公钥,该公钥对应于用于对特定令牌进行签名的私钥。 然后可以使用正确的公钥和指定的算法来执行签名验证。

注意

使用 kid 声明验证令牌。 虽然 v1.0 令牌同时包含 x5tkid 声明,但 v2.0 令牌仅包含 kid 声明。

本文档未说明如何执行签名验证。 有许多开放源代码库可用于帮助进行签名验证(如有必要)。 但是,Microsoft 标识平台具有一个针对标准的令牌签名扩展(它们是自定义签名密钥)。

如果应用程序因使用声明映射功能而拥有自定义签名密钥,请追加包含应用程序 ID 的 appid 查询参数。 对于验证,请使用指向应用程序的签名密钥信息的 jwks_uri。 例如:https://login.partner.microsoftonline.cn/{tenant}/.well-known/openid-configuration?appid=00001111-aaaa-2222-bbbb-3333cccc4444 包含 https://login.partner.microsoftonline.cn/{tenant}/discovery/keys?appid=00001111-aaaa-2222-bbbb-3333cccc4444jwks_uri

验证颁发者

验证 ID 令牌的 Web 应用和验证访问令牌的 Web API 需要对照下述项来验证令牌的颁发者(iss 声明):

  1. 与应用程序配置关联的 OpenID Connect 元数据文档中提供的颁发者(颁发机构)。 要验证的元数据文档取决于:
    • 令牌的版本
    • 应用程序支持的帐户。
  2. 令牌的租户 ID(tid 声明),
  3. 签名密钥的颁发者。

单租户应用程序

OpenID Connect Core 显示“颁发者标识符 [...] 必须与 iss(颁发者)声明的值完全匹配。”对于使用特定于租户的元数据终结点的应用程序,例如 https://login.partner.microsoftonline.cn/{example-tenant-id}/v2.0/.well-known/openid-configurationhttps://login.partner.microsoftonline.cn/contoso.partner.onmschina.cn/v2.0/.well-known/openid-configuration

单租户应用程序是支持以下项的应用程序:

  • 一个组织目录中的帐户(仅限 example-tenant-id):https://login.partner.microsoftonline.cn/{example-tenant-id}

多租户应用程序

Microsoft Entra ID 还支持多租户应用程序。 这些应用程序支持:

  • 任何组织目录中的帐户(任何 Microsoft Entra 目录):https://login.partner.microsoftonline.cn/organizations

对于这些应用程序,Microsoft Entra ID 分别在 https://login.partner.microsoftonline.cn/common/v2.0/.well-known/openid-configurationhttps://login.partner.microsoftonline.cn/organizations/v2.0/.well-known/openid-configuration 处公开与租户无关的 OIDC 文档版本。 这些终结点会返回颁发者值,该值是由 tenantid 参数化的模板:https://login.partner.microsoftonline.cn/{tenantid}/v2.0。 应用程序可以使用这些独立于租户租户的终结点,通过以下规定来验证每个租户的令牌:

  • 验证签名密钥颁发者
  • 应用程序不应期望令牌中的颁发者声明与元数据中的颁发者值完全匹配,而是应将颁发者元数据中的{tenantid}值替换为当前请求目标的租户 ID,然后检查完全匹配(令牌的tid声明)。
  • 验证 tid 声明是 GUID,而 iss 声明的形式是 https://login.partner.microsoftonline.cn/{tid}/v2.0,其中 {tid} 是确切的 tid 声明。 这种验证会反过来将租户与颁发者联系起来,并返回到创建信任链的签名密钥的范围。
  • 当他们找到与声明主题关联的数据时,请使用tid声明。 换句话说,tid 声明必须是用于访问用户数据的密钥的一部分。

验证签名密钥颁发者

使用 v2.0 与租户无关的元数据的应用程序还需要验证签名密钥颁发者。

密钥文档和签名密钥颁发者

如前所述,在 OpenID Connect 文档中,应用程序访问用于对令牌进行签名的密钥。 它通过访问在 OpenIdConnect 文档的 jwks_uri 属性中公开的 URL 来获取相应的密钥文档。

 "jwks_uri": "https://login.partner.microsoftonline.cn/{example-tenant-id}/discovery/v2.0/keys",

{example-tenant-id} 值可以替换为 GUID、域名,或者替换为“公用”、“组织”和“使用者

对于每个密钥,Azure AD v2.0 公开的 keys 文档包含使用此签名密钥的颁发者。 例如,独立于租户的“公用”密钥终结点 https://login.partner.microsoftonline.cn/common/discovery/v2.0/keys 返回如下所示的文档:

{
  "keys":[
    {"kty":"RSA","use":"sig","kid":"A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u","x5t":"A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u","n":"spv...","e":"AQAB","x5c":["MIID..."],"issuer":"https://login.partner.microsoftonline.cn/{tenantid}/v2.0"},
    {"kty":"RSA","use":"sig","kid":"C2dE3fH4iJ5kL6mN7oP8qR9sT0uV1w","x5t":"C2dE3fH4iJ5kL6mN7oP8qR9sT0uV1w","n":"wEM...","e":"AQAB","x5c":["MIID..."],"issuer":"https://login.partner.microsoftonline.cn/{tenantid}/v2.0"},
    {"kty":"RSA","use":"sig","kid":"E3fH4iJ5kL6mN7oP8qR9sT0uV1wX2y","x5t":"E3fH4iJ5kL6mN7oP8qR9sT0uV1wX2y","n":"rv0...","e":"AQAB","x5c":["MIID..."],"issuer":"https://login.partner.microsoftonline.cn/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"}
  ]
}

验证签名密钥颁发者

应用程序应使用与用于对令牌进行签名的密钥关联的密钥文档的 issuer 属性,来限制密钥的范围:

  • 只有当令牌中的 https://login.partner.microsoftonline.cn/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 声明与颁发者值完全匹配时,才应使用具有 GUID 类似于 iss 的颁发者值的密钥。
  • 只有在令牌中的iss声明在用令牌中的tid声明替换{tenantid}占位符后与此值匹配时,才应使用具有https://login.partner.microsoftonline.cn/{tenantid}/v2.0等模板化颁发者值的密钥。

对于接受来自多个租户的令牌的应用程序,使用独立于租户的元数据会更高效。

注意

使用独立于 Microsoft Entra 租户的元数据时,声明应在租户中解释,就像在标准 OpenID Connect 下,声明在颁发者中解释一样。 也就是说,{"sub":"ABC123","iss":"https://login.partner.microsoftonline.cn/{example-tenant-id}/v2.0","tid":"{example-tenant-id}"}{"sub":"ABC123","iss":"https://login.partner.microsoftonline.cn/{another-tenand-id}/v2.0","tid":"{another-tenant-id}"} 描述不同的用户,即使 sub 是相同的,因为像 sub 这样的声明是在颁发者/租户的上下文中解释的。

概括

下面是一些伪代码,它概括了如何验证颁发者和签名密钥颁发者:

  1. 从配置的元数据 URL 中提取密钥
  2. 如果使用已发布的密钥之一进行签名,则检查令牌;否则会失败
  3. 根据 kid 标头标识元数据中的密钥。 检查附加到元数据文档中密钥的“issuer”属性:
    var issuer = metadata["kid"].issuer;
    if (issuer.contains("{tenantId}", CaseInvariant)) issuer = issuer.Replace("{tenantid}", token["tid"], CaseInvariant);
    if (issuer != token["iss"]) throw validationException;
    if (configuration.allowedIssuer != "*" && configuration.allowedIssuer != issuer) throw validationException;
    var issUri = new Uri(token["iss"]);
    if (issUri.Segments.Count < 1) throw validationException;
    if (issUri.Segments[1] != token["tid"]) throw validationException;