如何验证此 ADFS 令牌?
How can I validate this ADFS token?
在我的 MVC 站点上,如果我检测到正在使用 ADFS 帐户,我会重定向到 ADFS 登录页面。用户输入他们的 ADFS 凭据后,ADFS 站点将 WsFederationMessage
发回我的站点。如何验证作为此 WsFederationMessage
的一部分提供给我的站点的 ADFS 令牌?
在 AuthenticationHandler
中间件 class 中,我有以下调用 ValidateToken
方法的相关代码:
IFormCollection form = await Request.ReadFormAsync();
WsFederationMessage wsFederationMessage = new WsFederationMessage(form);
if (!wsFederationMessage.IsSignInMessage)
{
Request.Body.Seek(0, SeekOrigin.Begin);
return null;
}
var token = wsFederationMessage.GetToken();
if (wsFederationMessage.Wresult != null && Options.SecurityTokenHandlers.CanReadToken(token))
{
SecurityToken validatedToken;
ClaimsPrincipal principal = Options.SecurityTokenHandlers.ValidateToken(token, Options.TokenValidationParameters, out validatedToken);
...
}
当我尝试调用 ValidateToken
:
时出现此错误
Description: An unhandled exception occurred during the execution of
the current web request. Please review the stack trace for more
information about the error and where it originated in the code.
Exception Details:
System.IdentityModel.SignatureVerificationFailedException: ID4037: The
key needed to verify the signature could not be resolved from the
following security key identifier 'SecurityKeyIdentifier (
IsReadOnly = False, Count = 1, Clause[0] =
X509RawDataKeyIdentifierClause(RawData = [Removed by Author]. Ensure
that the SecurityTokenResolver is populated with the required key.
搜索解决方案时,我找到了 this article, so I decoded the X509Certificate
presented within the token
string object in my code above using this site's OpenSSL-based decoder,因为它是在返回的 token
字符串的 <X509Certificate></X509Certificate>
XAML 标记内进行 PEM 编码的。事实上,这是解决方案文章所说的签名证书。所以我继续我的 ADFS 服务器,将签名证书导出为 public 证书并将其安装在我网站的 Trusted Root Certificate Authorities
上。 link 还提到我必须:
Import the certificate to the Signature tab of the RP Trust
因此,我将签名证书添加到我的 ADFS 服务器上我的依赖方信任的签名选项卡中,我在其中对我的机器标识符有一个信任规则。这一切之后,它仍然没有用。虽然有一点背景知识,但我的网站是 运行 在我的机器上本地通过 IIS,我已经更改了主机文件设置以使其指向 https://adfs-example.local/
。我的 ADFS 服务器目前通过 VPN 连接到我的站点,所以我的意思是 ADFS 服务器本身永远不会正确解析 https://adfs-example.local/
的标识符,如果它需要直接从这个 URI 请求一些东西,但是事情一旦浏览器重定向到我网站的登录页面并显示 ADFS 令牌,显然仍然有效。
再把我的上帝遗弃的头撞到墙上,我试着加上我自己的 IssuerSigningKeyResolver
:
TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKeyResolver = (token, securityToken, keyIdentifier, validationParameters) =>
{
var store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var cert = store.Certificates.Find(X509FindType.FindByThumbprint, "<My Certificate's Thumbprint>", true)[0];
store.Close();
var provider = (RSACryptoServiceProvider)cert.PublicKey.Key;
return new RsaSecurityKey(provider);
}
};
现在我遇到了一个错误,不知道该怎么办:
IDX10213: SecurityTokens must be signed. SecurityToken: '{0}'.
Description: An unhandled exception occurred during the execution of
the current web request. Please review the stack trace for more
information about the error and where it originated in the code.
Exception Details:
System.IdentityModel.Tokens.SecurityTokenValidationException:
IDX10213: SecurityTokens must be signed. SecurityToken: '{0}'.
Source Error:
Line 61: Line 62: var validatedToken =
(SecurityToken)null; Line 63: var principal =
Options.SecurityTokenHandlers.ValidateToken(token,
Options.TokenValidationParameters, out validatedToken); Line 64:
var claimsIdentity = principal.Identity as ClaimsIdentity; Line 65:
var ticket = new AuthenticationTicket(claimsIdentity, null);
处理程序被调用了两次。在第一次调用时,这似乎成功了。似乎第一个令牌已签名。在第二次调用时,它失败了。似乎第二个令牌未签名。为什么我的一些安全令牌没有签名?我该如何进一步调试呢?任何人都必须处理这样的事情吗?
现在我别无选择,只能检查源代码,所以我提取了 AzureAD 的整个主干(也称为 Wilson),我正在检查代码。它在 SAML 安全令牌处理程序的这一行失败:
if (samlToken.Assertion.SigningToken == null && validationParameters.RequireSignedTokens)
{
throw new SecurityTokenValidationException(ErrorMessages.IDX10213);
}
我不明白。这意味着签名令牌为空。为什么签名令牌为空?
编辑:再次检查 ADFS 服务器,我认为设置它的人忘记将私钥包含在 "Token-signing" 和 "Token-decrypting" 中属于 AD FS -> 服务 -> ADFS 管理单元的证书选项卡的证书。但奇怪的是,通过与设置它的人交谈,显然它需要服务证书并吐出另外两个用于令牌签名和解密......但没有他们的私钥?
编辑:根据this article,那两个"Token-signing"和"Token-decrypting"证书应该是有效的,因为它们是自动生成的,只是他们的私钥存储在 Active Directory 中:
When you use the self-signed certificates for token signing and
decryption, the private keys are stored in Active Directory in the
following container:
CN=ADFS,CN=Microsoft,CN=Program Data,DC=domain,DC=com
Consequently, for the ADFS installation to install the private keys
into this location, you must be a domain admin to install ADFS or have
the appropriate rights assigned to this container.
最后,我放弃了 AzureAD Nuget 包,它无缘无故地引起了头痛。我采用了直接的方法。现在我只是要求我的 AD FS 服务器验证用户凭据。这是代码(请务必安装 Windows Identity Foundation SDK,并添加对 Microsoft.IdentityModel.dll
、System.IdentityModel.dll
和 System.ServiceModel.dll
的引用):
using System;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Xml;
using Microsoft.IdentityModel.Protocols.WSTrust;
using Microsoft.IdentityModel.Protocols.WSTrust.Bindings;
namespace ADFSFederationToken
{
class Program
{
static string _relyingPartyIdentifier = "https://yourapplication.local/"; // Must be whatever you specified on your AD FS server as the relying party address.
static string _adfsServerAddress = "https://adfs.example.local/"; // Your ADFS server's address.
static string _username = "username@domain.local"; // A username to your ADFS server.
static string _password = "password"; // A password to your ADFS server.
static string _signingCertificateThumbprint = "1337..."; // Put the public ADFS Token Signing Certificate's thumbprint here and be sure to add it to your application's trusted certificates in the Certificates snap-in of MMC.
static string _signingCertificateCommonName = "ADFS Signing - adfs.example.local"; // Put the common name of the ADFS Token Signing Certificate here.
static void Main(string[] args)
{
Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannelFactory factory = null;
try
{
_relyingPartyIdentifier = _relyingPartyIdentifier.EndsWith("/") ? _relyingPartyIdentifier : _relyingPartyIdentifier + "/";
_adfsServerAddress = _adfsServerAddress.EndsWith("/") ? _adfsServerAddress : _adfsServerAddress + "/";
factory = new Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential), new EndpointAddress(_adfsServerAddress + "adfs/services/trust/13/usernamemixed"));
factory.TrustVersion = TrustVersion.WSTrust13;
factory.Credentials.UserName.UserName = _username;
factory.Credentials.UserName.Password = _password;
var rst = new Microsoft.IdentityModel.Protocols.WSTrust.RequestSecurityToken
{
RequestType = WSTrust13Constants.RequestTypes.Issue,
AppliesTo = new EndpointAddress(_relyingPartyIdentifier),
KeyType = WSTrust13Constants.KeyTypes.Bearer
};
var channel = factory.CreateChannel();
var genericToken = channel.Issue(rst) as GenericXmlSecurityToken;
var handler = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
var tokenString = genericToken.TokenXml.OuterXml;
var samlToken = handler.ReadToken(new XmlTextReader(new StringReader(tokenString)));
ValidateSamlToken(samlToken);
}
finally
{
if (factory != null)
{
try
{
factory.Close();
}
catch (CommunicationObjectFaultedException)
{
factory.Abort();
}
}
}
}
public static ClaimsIdentity ValidateSamlToken(SecurityToken securityToken)
{
var configuration = new SecurityTokenHandlerConfiguration();
configuration.AudienceRestriction.AudienceMode = AudienceUriMode.Always;
configuration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(_relyingPartyIdentifier));
configuration.CertificateValidationMode = X509CertificateValidationMode.ChainTrust;
configuration.RevocationMode = X509RevocationMode.Online;
configuration.CertificateValidator = X509CertificateValidator.ChainTrust;
var registry = new ConfigurationBasedIssuerNameRegistry();
registry.AddTrustedIssuer(_signingCertificateThumbprint, _signingCertificateCommonName);
configuration.IssuerNameRegistry = registry;
var handler = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection(configuration);
var identity = handler.ValidateToken(securityToken).First();
return identity;
}
}
}
编辑:如果我想从 AzureAD NuGet 包开始工作并继续重定向并使用它们的表单 post 请求解析器,我仍然可以使用上面的代码。我仍然可以读取 XAML 令牌字符串并解析为有效的 SecurityToken
对象,如下所示:
var token = wsFederationMessage.GetToken();
var samlToken = handler.ReadToken(new XmlTextReader(new StringReader(token)));
ValidateSamlToken(samlToken);
在我的 MVC 站点上,如果我检测到正在使用 ADFS 帐户,我会重定向到 ADFS 登录页面。用户输入他们的 ADFS 凭据后,ADFS 站点将 WsFederationMessage
发回我的站点。如何验证作为此 WsFederationMessage
的一部分提供给我的站点的 ADFS 令牌?
在 AuthenticationHandler
中间件 class 中,我有以下调用 ValidateToken
方法的相关代码:
IFormCollection form = await Request.ReadFormAsync();
WsFederationMessage wsFederationMessage = new WsFederationMessage(form);
if (!wsFederationMessage.IsSignInMessage)
{
Request.Body.Seek(0, SeekOrigin.Begin);
return null;
}
var token = wsFederationMessage.GetToken();
if (wsFederationMessage.Wresult != null && Options.SecurityTokenHandlers.CanReadToken(token))
{
SecurityToken validatedToken;
ClaimsPrincipal principal = Options.SecurityTokenHandlers.ValidateToken(token, Options.TokenValidationParameters, out validatedToken);
...
}
当我尝试调用 ValidateToken
:
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.IdentityModel.SignatureVerificationFailedException: ID4037: The key needed to verify the signature could not be resolved from the following security key identifier 'SecurityKeyIdentifier (
IsReadOnly = False, Count = 1, Clause[0] = X509RawDataKeyIdentifierClause(RawData = [Removed by Author]. Ensure that the SecurityTokenResolver is populated with the required key.
搜索解决方案时,我找到了 this article, so I decoded the X509Certificate
presented within the token
string object in my code above using this site's OpenSSL-based decoder,因为它是在返回的 token
字符串的 <X509Certificate></X509Certificate>
XAML 标记内进行 PEM 编码的。事实上,这是解决方案文章所说的签名证书。所以我继续我的 ADFS 服务器,将签名证书导出为 public 证书并将其安装在我网站的 Trusted Root Certificate Authorities
上。 link 还提到我必须:
Import the certificate to the Signature tab of the RP Trust
因此,我将签名证书添加到我的 ADFS 服务器上我的依赖方信任的签名选项卡中,我在其中对我的机器标识符有一个信任规则。这一切之后,它仍然没有用。虽然有一点背景知识,但我的网站是 运行 在我的机器上本地通过 IIS,我已经更改了主机文件设置以使其指向 https://adfs-example.local/
。我的 ADFS 服务器目前通过 VPN 连接到我的站点,所以我的意思是 ADFS 服务器本身永远不会正确解析 https://adfs-example.local/
的标识符,如果它需要直接从这个 URI 请求一些东西,但是事情一旦浏览器重定向到我网站的登录页面并显示 ADFS 令牌,显然仍然有效。
再把我的上帝遗弃的头撞到墙上,我试着加上我自己的 IssuerSigningKeyResolver
:
TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKeyResolver = (token, securityToken, keyIdentifier, validationParameters) =>
{
var store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var cert = store.Certificates.Find(X509FindType.FindByThumbprint, "<My Certificate's Thumbprint>", true)[0];
store.Close();
var provider = (RSACryptoServiceProvider)cert.PublicKey.Key;
return new RsaSecurityKey(provider);
}
};
现在我遇到了一个错误,不知道该怎么办:
IDX10213: SecurityTokens must be signed. SecurityToken: '{0}'.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.IdentityModel.Tokens.SecurityTokenValidationException: IDX10213: SecurityTokens must be signed. SecurityToken: '{0}'.
Source Error:
Line 61: Line 62: var validatedToken = (SecurityToken)null; Line 63: var principal = Options.SecurityTokenHandlers.ValidateToken(token, Options.TokenValidationParameters, out validatedToken); Line 64:
var claimsIdentity = principal.Identity as ClaimsIdentity; Line 65:
var ticket = new AuthenticationTicket(claimsIdentity, null);
处理程序被调用了两次。在第一次调用时,这似乎成功了。似乎第一个令牌已签名。在第二次调用时,它失败了。似乎第二个令牌未签名。为什么我的一些安全令牌没有签名?我该如何进一步调试呢?任何人都必须处理这样的事情吗?
现在我别无选择,只能检查源代码,所以我提取了 AzureAD 的整个主干(也称为 Wilson),我正在检查代码。它在 SAML 安全令牌处理程序的这一行失败:
if (samlToken.Assertion.SigningToken == null && validationParameters.RequireSignedTokens)
{
throw new SecurityTokenValidationException(ErrorMessages.IDX10213);
}
我不明白。这意味着签名令牌为空。为什么签名令牌为空?
编辑:再次检查 ADFS 服务器,我认为设置它的人忘记将私钥包含在 "Token-signing" 和 "Token-decrypting" 中属于 AD FS -> 服务 -> ADFS 管理单元的证书选项卡的证书。但奇怪的是,通过与设置它的人交谈,显然它需要服务证书并吐出另外两个用于令牌签名和解密......但没有他们的私钥?
编辑:根据this article,那两个"Token-signing"和"Token-decrypting"证书应该是有效的,因为它们是自动生成的,只是他们的私钥存储在 Active Directory 中:
When you use the self-signed certificates for token signing and decryption, the private keys are stored in Active Directory in the following container:
CN=ADFS,CN=Microsoft,CN=Program Data,DC=domain,DC=com
Consequently, for the ADFS installation to install the private keys into this location, you must be a domain admin to install ADFS or have the appropriate rights assigned to this container.
最后,我放弃了 AzureAD Nuget 包,它无缘无故地引起了头痛。我采用了直接的方法。现在我只是要求我的 AD FS 服务器验证用户凭据。这是代码(请务必安装 Windows Identity Foundation SDK,并添加对 Microsoft.IdentityModel.dll
、System.IdentityModel.dll
和 System.ServiceModel.dll
的引用):
using System;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Xml;
using Microsoft.IdentityModel.Protocols.WSTrust;
using Microsoft.IdentityModel.Protocols.WSTrust.Bindings;
namespace ADFSFederationToken
{
class Program
{
static string _relyingPartyIdentifier = "https://yourapplication.local/"; // Must be whatever you specified on your AD FS server as the relying party address.
static string _adfsServerAddress = "https://adfs.example.local/"; // Your ADFS server's address.
static string _username = "username@domain.local"; // A username to your ADFS server.
static string _password = "password"; // A password to your ADFS server.
static string _signingCertificateThumbprint = "1337..."; // Put the public ADFS Token Signing Certificate's thumbprint here and be sure to add it to your application's trusted certificates in the Certificates snap-in of MMC.
static string _signingCertificateCommonName = "ADFS Signing - adfs.example.local"; // Put the common name of the ADFS Token Signing Certificate here.
static void Main(string[] args)
{
Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannelFactory factory = null;
try
{
_relyingPartyIdentifier = _relyingPartyIdentifier.EndsWith("/") ? _relyingPartyIdentifier : _relyingPartyIdentifier + "/";
_adfsServerAddress = _adfsServerAddress.EndsWith("/") ? _adfsServerAddress : _adfsServerAddress + "/";
factory = new Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential), new EndpointAddress(_adfsServerAddress + "adfs/services/trust/13/usernamemixed"));
factory.TrustVersion = TrustVersion.WSTrust13;
factory.Credentials.UserName.UserName = _username;
factory.Credentials.UserName.Password = _password;
var rst = new Microsoft.IdentityModel.Protocols.WSTrust.RequestSecurityToken
{
RequestType = WSTrust13Constants.RequestTypes.Issue,
AppliesTo = new EndpointAddress(_relyingPartyIdentifier),
KeyType = WSTrust13Constants.KeyTypes.Bearer
};
var channel = factory.CreateChannel();
var genericToken = channel.Issue(rst) as GenericXmlSecurityToken;
var handler = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
var tokenString = genericToken.TokenXml.OuterXml;
var samlToken = handler.ReadToken(new XmlTextReader(new StringReader(tokenString)));
ValidateSamlToken(samlToken);
}
finally
{
if (factory != null)
{
try
{
factory.Close();
}
catch (CommunicationObjectFaultedException)
{
factory.Abort();
}
}
}
}
public static ClaimsIdentity ValidateSamlToken(SecurityToken securityToken)
{
var configuration = new SecurityTokenHandlerConfiguration();
configuration.AudienceRestriction.AudienceMode = AudienceUriMode.Always;
configuration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(_relyingPartyIdentifier));
configuration.CertificateValidationMode = X509CertificateValidationMode.ChainTrust;
configuration.RevocationMode = X509RevocationMode.Online;
configuration.CertificateValidator = X509CertificateValidator.ChainTrust;
var registry = new ConfigurationBasedIssuerNameRegistry();
registry.AddTrustedIssuer(_signingCertificateThumbprint, _signingCertificateCommonName);
configuration.IssuerNameRegistry = registry;
var handler = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection(configuration);
var identity = handler.ValidateToken(securityToken).First();
return identity;
}
}
}
编辑:如果我想从 AzureAD NuGet 包开始工作并继续重定向并使用它们的表单 post 请求解析器,我仍然可以使用上面的代码。我仍然可以读取 XAML 令牌字符串并解析为有效的 SecurityToken
对象,如下所示:
var token = wsFederationMessage.GetToken();
var samlToken = handler.ReadToken(new XmlTextReader(new StringReader(token)));
ValidateSamlToken(samlToken);