On my MVC site, I redirect to an ADFS login page if I detect an ADFS account is being used. After the user enters their ADFS credentials, the ADFS site posts a WsFederationMessage
back to my site. How can I validate the ADFS token that is presented to my site as part of this WsFederationMessage
?
Inside of an AuthenticationHandler
middleware class, I have the following relevant code which calls the ValidateToken
method:
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);
...
}
I got this error when I tried to call 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.
Searching for a resolution, I found this article, so I decoded the X509Certificate
presented within the token
string object in my code above using this site's OpenSSL-based decoder, as it was PEM-encoded within the <X509Certificate></X509Certificate>
XAML tag of the returned token
string. Indeed, it was the signing certificate as the resolution article says it should be. So I went on my ADFS server, exported the signing certificate as a public certificate and installed it on my website's Trusted Root Certificate Authorities
. The link also mentioned that I have to:
Import the certificate to the Signature tab of the RP Trust
So I added the signing certificate to the Signature tab of my Relying Party Trust on my ADFS server where I have a trust rule for my machine's identifier. After all of this, it still didn't work. A little bit of background though, my website is running locally on my machine over IIS and I've changed hosts file settings to make it point to https://adfs-example.local/
. My ADFS server is VPN connected to my site at the moment, so what I'm saying is the ADFS server itself will never properly resolve an identifier of https://adfs-example.local/
if it ever needs to request something from this URI directly, but things still obviously work once it browser redirects to my site's sign-on page and presents an ADFS token.
Bashing my God forsaken head against the wall some more, I tried adding my own 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);
}
};
Now I have this beauty of an error, and no clue what to do with it:
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);
The handler gets called twice. On the first call, this seems to succeed. It seems the first token is signed. On the second call, it fails. It seems the second token is NOT signed. Why aren't some of my security tokens signed? How can I debug this further? Anyone ever have to deal with something like this?
Now I've no choice but to check the source, so I pulled the entire trunk of AzureAD (otherwise known as Wilson) and I'm going through the code. It fails at this line of the SAML security token handler:
if (samlToken.Assertion.SigningToken == null && validationParameters.RequireSignedTokens)
{
throw new SecurityTokenValidationException(ErrorMessages.IDX10213);
}
I don't understand. This means that the signing token is null. Why is the signing token null?
Edit: Checking the ADFS server again, I think whoever set it up forgot to include the private key as part of the "Token-signing" and "Token-decrypting" certificates which are part of the AD FS -> Service -> Certificates tab of the ADFS snap-in. But oddly enough, from talking to the guy who set it up, apparently it takes the service certificate and spits out the other two for token signing and decrypting...without their private keys though?
Edit: According to this article, those two "Token-signing" and "Token-decrypting" certificates should be valid as they have been auto-generated, just that their private keys are stored in 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.
In the end, I dropped the AzureAD Nuget package, it caused nothing but headaches for no good reason. I went with a direct approach. Now I just simply ask my AD FS server to validate user credentials. Here is the code (just be sure to have the Windows Identity Foundation SDK installed, and add references to Microsoft.IdentityModel.dll
, System.IdentityModel.dll
, and 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 = "[email protected]"; // 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;
}
}
}
Edit: If I wanted to work from the AzureAD NuGet package and continue to redirect and to use their form post request parser, I can still do this using the above code. I can still read the XAML token string and parse into a valid SecurityToken
object like so:
var token = wsFederationMessage.GetToken();
var samlToken = handler.ReadToken(new XmlTextReader(new StringReader(token)));
ValidateSamlToken(samlToken);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With