I am trying to validate a SAML token that is sent as a header to a ASP.NET Core 6 Web API. Frankly, I do not have a lot of experience working with tokens, so I probably lack some foundational knowledge that would be helpful here. Any guidance is appreciated.
Tokens are acquired from a .NET Framework service like which obtains them from our ADFS 3.0 instance on Windows Server 2012R2 using code like the following:
WSTrustChannelFactory trustChannelFactory =
new WSTrustChannelFactory(new WindowsWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(new Uri(url)));
trustChannelFactory.TrustVersion = TrustVersion.WSTrust13;
trustChannelFactory.Credentials.Windows.ClientCredential.Domain = domain;
trustChannelFactory.Credentials.Windows.ClientCredential.UserName = username;
trustChannelFactory.Credentials.Windows.ClientCredential.Password = password;
RequestSecurityToken rst = new RequestSecurityToken(WSTrust13Constants.RequestTypes.Issue, WSTrust13Constants.KeyTypes.Bearer);
rst.AppliesTo = new EndpointAddress(relyingParting);
rst.TokenType = Microsoft.IdentityModel.Tokens.SecurityTokenTypes.Saml2TokenProfile11;
WSTrustChannel channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
GenericXmlSecurityToken token = channel.Issue(rst) as GenericXmlSecurityToken;
tokenString = token.TokenXml.OuterXml;
return tokenString;
The token comes out looking something like this:
<Assertion ID="_1d020948-dd8c-4349-9b4e-94832d2381b3" IssueInstant="2024-04-29T19:18:26.948Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
<Issuer>http://my-adfs-instance.net/adfs/services/trust</Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<ds:Reference URI="#_1d020948-dd8c-4349-9b4e-94832d2381b3">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<ds:DigestValue>NoClueIfSensitive</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>Redacted</ds:SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MII...</ds:X509Certificate>
</ds:X509Data>
</KeyInfo>
</ds:Signature>
<Subject>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData NotOnOrAfter="2024-04-29T19:23:26.948Z" />
</SubjectConfirmation>
</Subject>
<Conditions NotBefore="2024-04-29T19:18:26.653Z" NotOnOrAfter="2024-04-29T20:18:26.653Z">
<AudienceRestriction>
<Audience>https://my-app.net</Audience>
</AudienceRestriction>
</Conditions>
<AttributeStatement>
...
</AttributeStatement>
<AuthnStatement AuthnInstant="2024-04-29T19:18:26.590Z">
...
</AuthnStatement>
</Assertion>
I've abbreviated some fields for brevity since its already pretty long - just let me know if those things are important for an answer.
That token is sent over as a header to the ASP.NET Core Web API I mentioned.
I have tried a couple different ways of validating this token.
Ideally, I'd like to use a Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler
so that I can leverage the ValidateToken
method that comes with it like so:
var headers = Request.Headers;
var token = headers["token"];
var tokenHandler = new Saml2SecurityTokenHandler();
var tokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = _config.GetSection("TokenValidation")["ValidIssuer"],
ValidAudience = _config.GetSection("TokenValidation")["ValidAudience"]
};
tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
This fails, which I believe is to be expected, with a SecurityTokenSignatureKeyNotFoundException
exception that states
IDX10500: Signature validation failed. No security keys were provided to validate the signature
After rummaging around documentation, I then tried the IssuerSigningKey
property out by pulling down the code-signing cert from my ADFS instance and placing it on my local machine. Not a perfect solution, but just trying to get some kind of successful test at this point:
var tokenHandler = new Saml2SecurityTokenHandler();
var issuerSigningCert = new X509Certificate2("C:\\signingCert.cer");
var securityKey = new X509SecurityKey(issuerSigningCert);
var tokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = _config.GetSection("TokenValidation")["ValidIssuer"],
ValidAudience = _config.GetSection("TokenValidation")["ValidAudience"],
IssuerSigningKey = securityKey
};
tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
This results in a SecurityTokenInvalidSignatureException
with message
IDX10514: Signature validation failed
I also tried plugging in the PFX instead of just the cer by changing the file path to the pfx and supplying the password in the X509Certificate2
constructor, but I received the same exception and message.
I did notice something interesting when trying to use the ReadSaml2Token
method:
var tokenHandler = new Saml2SecurityTokenHandler();
using (XmlReader reader = XmlReader.Create(new StringReader(token)))
{
var tokenReadIn = tokenHandler.ReadSaml2Token(reader);
}
The SecurityKey
and SigningKey
are always null. This makes me wonder if there is some issue with my tokens in particular that they aren't being parsed correctly, and this is where I am stuck with this route.
I have a separate .NET Framework 4.5 API that is able to receive and validate tokens using the following code
Microsoft.IdentityModel.Configuration.ServiceConfiguration serviceConfig
= new Microsoft.IdentityModel.Configuration.ServiceConfiguration();
SecurityToken theToken = null;
ClaimsIdentityCollection identity = null;
using (XmlReader reader = XmlReader.Create(new StringReader(authSamlString)))
{
theToken = serviceConfig.SecurityTokenHandlers.ReadToken(reader);
identity = serviceConfig.SecurityTokenHandlers.ValidateToken(theToken);
}
The packages used here are System.IdentityModel.Tokens
and Microsoft.IdentityModel.Claims
and they don't appear to be intended for .NET Core/.NET so I wasn't able to replicate that exactly, but I don't see any specific customization or configuration other than just the ValidateToken
call in this application.
The alternate route I took involved using SignedXml
to load in the Signature, specify the cert public key, and attempt to check with CheckSignature
, very similar to the answer posted here Asp.Net Core SAML Response Signature Validation.
This was quite a mess, and I'd greatly prefer to avoid that route unless it's my only option. No matter what I tried with that route, I also could not get anything other then a false
result from CheckSignature
. I can post the actual code I used if that seems necessary, but I'd like to see if I can get the Microsoft.IdentityModel
solution working instead.
Any and all help is greatly appreciated. Upgrading ADFS is unfortunately not feasible, but if there are any configuration changes or alternate approaches in general, those are fine.
EDIT #1:
I tried an alternate way to specify the IssuerSigningKey
:
var issuerSigningCert = new X509Certificate2("C:\\signingCert.cer");
var securityKey = new X509SecurityKey(issuerSigningCert);
var tokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = _config.GetSection("TokenValidation")["ValidIssuer"],
ValidAudience = _config.GetSection("TokenValidation")["ValidAudience"],
IssuerSigningKey = new SymmetricSecurityKey(issuerSigningCert.GetPublicKey())
};
When I try to validate with this setup, I get an exception that states:
Message: IDX10512: Signature validation failed. Token does not have KeyInfo.
Even though my token does have KeyInfo
within it.
When I review the token after calling ReadSaml2Token
, here is what I see:
Everything is null except for the cert in the X509Data property.
Edit #2:
It looks like IDX10512
is possibly a red-herring. I turned on ShowPII
and my exception message grew to
Message "IDX10512: Signature validation failed. Token does not have KeyInfo. Keys tried: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: 'ABCD...'. , KeyId: \r\n'.\nExceptions caught:\n 'Microsoft.IdentityModel.Xml.XmlValidationException: IDX30207: SignatureMethod is not supported: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'. CryptoProviderFactory: 'Microsoft.IdentityModel.Tokens.CryptoProviderFactory'.\r\n at Microsoft.IdentityModel.Xml.Signature.Verify(SecurityKey key, CryptoProviderFactory cryptoProviderFactory)\r\n at Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateSignature(Saml2SecurityToken samlToken, String token, TokenValidationParameters validationParameters)\r\n'.\ntoken: '[Security Artifact of type 'Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/SecurityArtifactLogging.]'." string
So it appears to be a lack of support for rsa-sha256? No idea how to work around this one yet...
Well I will never get 25 hours of my life back but I did learn a valuable lesson.
There was nothing wrong with pretty much anything other than how I was sending the token for testing. My token source is a .NET Framework SOAP API and tends to spit things out a little funky.
This doesn't seem to happen when a program retrieves the token and sends a request, but when I am testing manually and obtain the token thru Postman, it does. To counter this issue in general for all requests, in my infinite wisdom, I wrote a tiny helper method to do string replacements on <
to <
and >
to >
. Apparently this is wildly unsafe, for reasons I am not yet sure of, though I am sure they are reasonable.
I finally stumbled on the appropriate way to do this which is System.Net.WebUtility.HtmlDecode
and would you look at that, I am able to validate my token.
At the end of the day, I guess the lesson is to work smarter and not harder. Working code sample:
var decodedToken = System.Net.WebUtility.HtmlDecode(token);
var tokenHandler = new Saml2SecurityTokenHandler();
var issuerSigningCert = new X509Certificate2("C:\\signingCert.cer");
var securityKey = new X509SecurityKey(issuerSigningCert);
var tokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = _config.GetSection("TokenValidation")["ValidIssuer"],
ValidAudience = _config.GetSection("TokenValidation")["ValidAudience"],
IssuerSigningKey = securityKey
};
var identity = tokenHandler.ValidateToken(decodedToken , tokenValidationParameters, out SecurityToken validatedToken);
I'll likely update my code to either pull the cert from a machine store or reach out to my ADFS instance for it directly, but you get the idea here.