本文介绍了你需要了解的有关 Microsoft 标识平台用来为安全令牌签名的公钥的信息。 请务必注意,这些密钥会定期滚动更新,并且在紧急情况下可立即滚动更新。 所有使用 Microsoft 标识平台的应用程序都应该能以编程方式处理密钥滚动更新过程。 你将了解密钥的工作原理,如何评估滚动更新对应用程序的影响。 你还将了解如何更新应用程序或建立定期手动滚动更新过程,以在必要时处理密钥滚动更新。
Microsoft 标识平台中签名密钥的概述
Microsoft 标识平台使用基于行业标准构建的公钥加密,在它自己和使用它的应用程序之间建立信任关系。 实际上,它的工作原理如下所述:Microsoft 标识平台使用签名密钥,该密钥由公钥和私钥对组成。 当用户登录到使用 Microsoft 标识平台进行身份验证的应用程序时,Microsoft 标识平台会创建一个包含用户相关信息的安全令牌。 此令牌由 Microsoft 标识平台使用其私钥进行签名,然后再发送回应用程序。 若要验证该令牌是否有效且来自 Microsoft 标识平台,应用程序必须使用由 Microsoft 标识平台公开的公钥(包含在租户的 OpenID Connect 发现文档或 SAML/WS-Fed 联合元数据文档中)来验证令牌的签名。
出于安全考虑,Microsoft 标识平台的签名密钥会定期更新,且紧急情况下,可立即滚动更新。 这些密钥滚动更新之间未设置时间或没有确切的时间。 任何与 Microsoft 标识平台集成的应用程序都应该做好处理密钥变换事件的准备,无论该事件可能发生的频率如何。 如果应用程序不处理突然刷新,并且尝试使用过期的密钥验证令牌上的签名,则应用程序将错误地拒绝令牌。 建议使用标准库来确保密钥元数据已正确刷新并保持最新状态。 如果未使用标准库,请确保实现遵循最佳做法部分。
OpenID Connect 发现文档和联合元数据文档中始终提供多个有效密钥。 应用程序应该做好使用文档中指定的任何以及所有密钥的准备,因为一个密钥可能很快会滚动更新,另一个密钥可能就会取而代之,依此类推。 由于我们支持新平台、新云或新的身份验证协议,目前的密钥数量随时间的推移,可能会根据 Microsoft 标识平台的内部体系结构发生变化。 JSON 响应中的密钥顺序和它们的公开顺序均不应被视为对应用程序有意义。 若要了解有关 JSON Web 密钥数据结构的详细信息,可以参考 RFC7517。
KeyRefresh(issuer)
{
// Store cache entries and last successful refresh timestamp per distinct 'issuer'if (LastSuccessfulRefreshTime issetand more recent than 5 minutes ago)
return// without refreshing// Load keys URI using the tenant-specific OIDC configuration endpoint ('issuer' is the input parameter)
oidcConfiguration = download JSON from"{issuer}/.well-known/openid-configuration"// Load list of keys from keys URI
keyList = download JSON from jwks_uri property of oidcConfiguration
foreach (key in keyList)
{
cache entry = lookup in cache by kid property of key
if (cache entry found)
set expiration of cache entry to now + 24h
elseadd key to cache with expiration set to now + 24h
}
set LastSuccessfulRefreshTime to now // current timestamp
}
服务启动过程:
用于更新密钥的 KeyRefresh
启动每小时调用 KeyRefresh 的后台作业
用于验证密钥的 TokenValidation 过程(伪代码):
C#
ValidateToken(token)
{
kid = token.header.kid // get key id from token header
issuer = token.body.iss // get issuer from 'iss' claim in token body
key = lookup in cache by issuer and kid
if (key found)
{
validate token with key andreturn
}
else// key is not found in the cache
{
call KeyRefresh(issuer) // to opportunistically refresh the keys for the issuer
key = lookup in cache by issuer and kid
if (key found)
{
validate token with key andreturn
}
else// key is not found in the cache even after refresh
{
return token validation error
}
}
}
如果在 Visual Studio 2013 中使用 Web API 模板创建了 Web API 应用程序,然后在“更改身份验证”菜单中选择了“组织帐户”,则应用程序中已包含必需的逻辑 。
如果是手动配置的身份验证,请参阅下面的说明,了解如何将 Web API 配置为自动更新其密钥信息。
以下代码片段演示如何从联合元数据文档获取最新密钥,并使用 JWT 令牌处理程序来验证令牌。 该代码片段假设你使用自己的缓存机制来持久保存密钥(以便验证将来从 Microsoft 标识平台获取的令牌),无论是将它保存在数据库中、配置文件中,还是保存在其他位置。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IdentityModel.Tokens;
using System.Configuration;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
using System.IdentityModel.Metadata;
using System.ServiceModel.Security;
using System.Threading;
namespace JWTValidation
{
public class JWTValidator
{
private string MetadataAddress = "[Your Federation Metadata document address goes here]";
// Validates the JWT Token that's part of the Authorization header in an HTTP request.
public void ValidateJwtToken(string token)
{
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
{
// Do not disable for production code
CertificateValidator = X509CertificateValidator.None
};
TokenValidationParameters validationParams = new TokenValidationParameters()
{
AllowedAudience = "[Your App ID URI goes here]",
ValidIssuer = "[The issuer for the token goes here, such as https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/]",
SigningTokens = GetSigningCertificates(MetadataAddress)
// Cache the signing tokens by your desired mechanism
};
Thread.CurrentPrincipal = tokenHandler.ValidateToken(token, validationParams);
}
// Returns a list of certificates from the specified metadata document.
public List<X509SecurityToken> GetSigningCertificates(string metadataAddress)
{
List<X509SecurityToken> tokens = new List<X509SecurityToken>();
if (metadataAddress == null)
{
throw new ArgumentNullException(metadataAddress);
}
using (XmlReader metadataReader = XmlReader.Create(metadataAddress))
{
MetadataSerializer serializer = new MetadataSerializer()
{
// Do not disable for production code
CertificateValidationMode = X509CertificateValidationMode.None
};
EntityDescriptor metadata = serializer.ReadMetadata(metadataReader) as EntityDescriptor;
if (metadata != null)
{
SecurityTokenServiceDescriptor stsd = metadata.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();
if (stsd != null)
{
IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First());
tokens.AddRange(x509DataClauses.Select(token => new X509SecurityToken(new X509Certificate2(token.GetX509RawData()))));
}
else
{
throw new InvalidOperationException("There is no RoleDescriptor of type SecurityTokenServiceType in the metadata");
}
}
else
{
throw new Exception("Invalid Federation Metadata document");
}
}
return tokens;
}
}
}