验证 Exchange 标识令牌
重要
旧版 Exchange 令牌已弃用。 从 2025 年 2 月开始,我们将开始关闭 Exchange Online 租户的旧版 Exchange 用户标识和回调令牌。 有关时间线和详细信息,请参阅常见问题解答页面。 这是 Microsoft的“安全未来计划”的一部分,该计划为组织提供了应对当前威胁环境所需的工具。 Exchange 用户标识令牌仍适用于本地 Exchange。 嵌套应用身份验证是今后令牌的建议方法。
Outlook 加载项可以向你发送 Exchange 用户标识令牌,但是在你信任此请求之前,必须验证该令牌以确保它来自预期的 Exchange 服务器。 Exchange 用户标识令牌均为 JSON Web 令牌 (JWT)。 RFC 7519 JSON Web 令牌 (JWT) 中介绍了验证 JWT 所需的步骤。
建议使用四个步骤验证标识令牌并获取用户的唯一标识符。 首先,从 base64 URL 编码的字符串中提取 JSON Web 令牌 (JWT)。 然后,确保该令牌格式正确、它是用于 Outlook 外接程序的令牌、它未过期且你可以提取身份验证元数据文档的有效 URL。 接下来,从 Exchange 服务器中检索身份验证元数据文档并验证附加到标识令牌的签名。 最后,通过将用户的 Exchange ID 与身份验证元数据文档的 URL 连接,计算用户的唯一标识符。
提取 JSON Web 令牌
getUserIdentityTokenAsync 返回的令牌是令牌的编码字符串表示形式。 在此表示形式下,根据 RFC 7519,所有 JWT 都有三个部分,以句点分隔。 格式如下所示。
{header}.{payload}.{signature}
标头和有效负载应进行 Base64 解码,以获取每一部分的 JSON 表示形式。 签名应进行 base64 解码,以获取包含二进制签名的字节数组。
有关令牌内容的详细信息,请参阅 Exchange 标识令牌揭秘。
三个组件都解码后,可以继续验证该令牌的内容。
验证令牌内容
若要验证令牌内容,应检查以下内容:
检查标头并验证:
-
typ
声明设置为JWT
。 -
alg
声明设置为RS256
。 -
x5t
声明存在。
-
检查有效负载并验证:
-
amurl
中的appctx
声明设置为授权令牌签名密钥清单文件的位置。 例如,Microsoft 365 https://outlook.office365.com:443/autodiscover/metadata/json/1的预期amurl
值为 。 有关其他信息,请参阅下一部分 验证域 。 - 当前时间介于 和
exp
声明中指定的nbf
时间之间。nbf
声明指定了令牌被视为有效的最早时间,而exp
声明指定了令牌的失效时间。 建议将服务器之间的时钟设置差异考虑在内。 -
aud
claim 是外接程序的预期 URL。 -
version
声明内的appctx
声明设置为ExIdTok.V1
。
-
验证域
实现上一部分所述的验证逻辑时,还必须要求声明的 amurl
域与用户的自动发现域匹配。 为此,需要使用或实现 Exchange 的自动发现。
对于Exchange Online,请
amurl
确认 是已知域 (https://outlook.office365.com:443/autodiscover/metadata/json/1) ,或属于特定于地理位置的云或专用云 (Office 365 URL 和 IP 地址范围) 。如果外接程序服务具有用户租户的预先存在的配置,则可以确定这
amurl
是否受信任。对于 Exchange 混合部署,请使用基于 OAuth 的自动发现来验证用户预期的域。 但是,尽管用户需要作为自动发现流的一部分进行身份验证,但外接程序绝不应收集用户的凭据并执行基本身份验证。
如果外接程序无法使用这些选项中的任何一个来验证 amurl
,则可以选择正常关闭加载项,并向用户发送相应的通知(如果加载项的工作流需要身份验证)。
验证标识令牌签名
知道 JWT 包含必需的声明后,便可以继续验证令牌签名。
检索公用签名密钥
第一步是检索 Exchange 服务器用于为令牌签名的证书对应的公钥。 可在身份验证元数据文档中找到此公钥。 此文档是托管在 amurl
声明中指定的 URL 上的一个 JSON 文件。
身份验证元数据文档使用以下格式。
{
"id": "_70b34511-d105-4e2b-9675-39f53305bb01",
"version": "1.0",
"name": "Exchange",
"realm": "*",
"serviceName": "00000002-0000-0ff1-ce00-000000000000",
"issuer": "00000002-0000-0ff1-ce00-000000000000@*",
"allowedAudiences": [
"00000002-0000-0ff1-ce00-000000000000@*"
],
"keys": [
{
"usage": "signing",
"keyinfo": {
"x5t": "enh9BJrVPU5ijV1qjZjV-fL2bco"
},
"keyvalue": {
"type": "x509Certificate",
"value": "MIIHNTCC..."
}
}
],
"endpoints": [
{
"location": "https://by2pr06mb2229.namprd06.prod.outlook.com:444/autodiscover/metadata/json/1",
"protocol": "OAuth2",
"usage": "metadata"
}
]
}
可用签名密钥位于 keys
数组中。 通过确保 keyinfo
属性中的 x5t
值与令牌标头中的 x5t
值相匹配,来选择正确的密钥。 公钥位于 keyvalue
属性中的 value
属性内,被存储为 Base64 编码的字节数组。
拥有正确的公钥后,验证此签名。 签名数据是已编码的令牌的前两个部分,用句点分隔:
{header}.{payload}
计算 Exchange 帐户的唯一 ID
通过将身份验证元数据文档 URL 与帐户的 Exchange 标识符连接在一起,为 Exchange 帐户创建唯一标识符。 如果具有此唯一标识符,请使用它为 Outlook 外接程序 Web 服务创建单一登录 (SSO) 系统。 有关将此唯一标识符用于 SSO 的详细信息,请参阅对具有 Exchange 标识令牌的用户进行身份验证。
使用库验证令牌
有许多库可以执行常规 JWT 解析和验证。 Microsoft提供了 System.IdentityModel.Tokens.Jwt
可用于验证 Exchange 用户标识令牌的库。
重要
我们不再建议使用 Exchange Web Services 托管 API,因为 Microsoft.Exchange.WebServices.Auth.dll 虽然仍然可用,但现在已过时,并且依赖于不受支持的库(如 Microsoft.IdentityModel.Extensions.dll)。
System.IdentityModel.Tokens.Jwt
System.IdentityModels.Tokens.Jwt 库可以解析令牌,也可以执行验证,但需要自行解析 appctx
声明并检索公用签名密钥。
// Load the encoded token
string encodedToken = "...";
JwtSecurityToken jwt = new JwtSecurityToken(encodedToken);
// Parse the appctx claim to get the auth metadata url
string authMetadataUrl = string.Empty;
var appctx = jwt.Claims.FirstOrDefault(claim => claim.Type == "appctx");
if (appctx != null)
{
var AppContext = JsonConvert.DeserializeObject<ExchangeAppContext>(appctx.Value);
// Token version check
if (string.Compare(AppContext.Version, "ExIdTok.V1", StringComparison.InvariantCulture) != 0) {
// Fail validation
}
authMetadataUrl = AppContext.MetadataUrl;
}
// Use System.IdentityModel.Tokens.Jwt library to validate standard parts
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters tvp = new TokenValidationParameters();
tvp.ValidateIssuer = false;
tvp.ValidateAudience = true;
tvp.ValidAudience = "{URL to add-in}";
tvp.ValidateIssuerSigningKey = true;
// GetSigningKeys downloads the auth metadata doc and
// returns a List<SecurityKey>
tvp.IssuerSigningKeys = GetSigningKeys(authMetadataUrl);
tvp.ValidateLifetime = true;
try
{
var claimsPrincipal = tokenHandler.ValidateToken(encodedToken, tvp, out SecurityToken validatedToken);
// If no exception, all standard checks passed
}
catch (SecurityTokenValidationException ex)
{
// Validation failed
}
ExchangeAppContext
类定义如下:
using Newtonsoft.Json;
/// <summary>
/// Representation of the appctx claim in an Exchange user identity token.
/// </summary>
public class ExchangeAppContext
{
/// <summary>
/// The Exchange identifier for the user
/// </summary>
[JsonProperty("msexchuid")]
public string ExchangeUid { get; set; }
/// <summary>
/// The token version
/// </summary>
public string Version { get; set; }
/// <summary>
/// The URL to download authentication metadata
/// </summary>
[JsonProperty("amurl")]
public string MetadataUrl { get; set; }
}
有关使用此库验证 Exchange 令牌并拥有 GetSigningKeys
实现的示例,请参阅 Outlook-Add-In-Token-Viewer。