I would like to validate a JWS token with "alg": "ES256", "x5c" header fields only.
x5c field contain 3 certificates. I'm able to load them but .net return a IDX10503 error telling me kid field is missing.
here is my code :
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(token);
var x5cList = JsonConvert.DeserializeObject<List<string>>(jwtSecurityToken.Header.X5c);
var certs = ValidateCertificate(x5cList.ToList());
var keys = certs.Select(cert => new X509SecurityKey(cert)).ToList();
SecurityToken validatedToken;
handler.ValidateToken(jwtSecurityToken.RawData, new TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = keys,
TryAllIssuerSigningKeys = true,
}, out validatedToken);
Anyone know why it's not working ? I also tried to don't load keys by myself thinking that JwtSecurityTokenHandler may be able to do it by it self but it's not working as well.
When I load this token on jwt.io it says it is validated...
Related
I would like just to verify JWT from Apple Pay server notification.
You can see the JWT structure on the screenshot from the jwt.io website.
So, I took the first certificate from the x5c collection in the header, and convert it to the object X509Certificate2, then I get the public key in the ECDsa format and try to verify the token.
Did I implement this correctly in terms of security?
Should I validate a chain of three certificates after verifying the token?
I will be grateful for any information.
private static Dictionary<string, string> GetClaimsByToken(string jwtToken)
{
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.ReadJwtToken(jwtToken);
token.Header.TryGetValue("x5c", out object x5c)
var certeficatesItems = JsonConvert.DeserializeObject<IEnumerable<string>>(x5c.ToString());
ValidateJWS(tokenHandler, jwtToken, certeficatesItems.First());
return token.Claims.ToDictionary(c => c.Type, v => v.Value);
}
private static void ValidateJWS(JwtSecurityTokenHandler tokenHandler, string jwtToken, string publicKey)
{
var certificateBytes = Base64UrlEncoder.DecodeBytes(publicKey);
var certificate = new X509Certificate2(certificateBytes);
var eCDsa = certificate.GetECDsaPublicKey();
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new ECDsaSecurityKey(eCDsa),
};
tokenHandler.ValidateToken(jwtToken, tokenValidationParameters, out var securityToken);
}
I don’t know C# but today I stumble across this problem too.
It seems so after you validate jwt token, you have to validate a chain of certificates. You need to download certificates from Apple(root and intermediate). You can find these certificates on apple site: https://www.apple.com/certificateauthority/.
You need to download them and validate them against the ones found in the x5c header. You can find more info on apple developer forum: https://developer.apple.com/forums/thread/691464
I am using a WebApp in Azure with Azure Active Directory authentication turned on and and am pulling the id_token from the /.auth/me page here:
From my understanding when I paste this into JWT.io it validates the signature by looking up the OpenId Configuration at https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration
and then uses the jwks_uri (https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys) to find the public signing key (x5c Array) based on the kid from the JWT header here:
However when I try to replicate this scenario in C# I am unable to validate the signature using the System.IdentityModel.Tokens.Jwt library.
Here are a few of my attempts and results:
ATTEMPT
var idToken = "{id_token}";
var signingKey = "{x5c[0]}";
SecurityToken validatedToken;
ClaimsPrincipal claimsPrincipal;
var handler = new JwtSecurityTokenHandler();
var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("-----BEGIN CERTIFICATE-----\n" + signingKey + "\n-----END CERTIFICATE-----"));
var tokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = symmetricSecurityKey,
ValidateIssuerSigningKey = true,
ValidateLifetime = false,
ValidateAudience = false,
ValidateIssuer = false
};
try
{
claimsPrincipal = handler.ValidateToken(idToken, tokenValidationParameters, out validatedToken);
Console.WriteLine($"{claimsPrincipal.Identity.Name}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
RESULT
IDX10501: Signature validation failed. Unable to match key: kid:
'System.String'. Exceptions caught: 'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
ATTEMPT
var kid = "l3sQ-50cCH4xBVZLHTGwnSR7680";
var idToken = "{id_token}";
var signingKey = "{x5c[0]}";
SecurityToken validatedToken;
ClaimsPrincipal claimsPrincipal;
var handler = new JwtSecurityTokenHandler();
var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("-----BEGIN CERTIFICATE-----\n" + signingKey + "\n-----END CERTIFICATE-----")) { KeyId = kid, };
var tokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = symmetricSecurityKey,
ValidateIssuerSigningKey = true,
ValidateLifetime = false,
ValidateAudience = false,
ValidateIssuer = false
};
try
{
claimsPrincipal = handler.ValidateToken(idToken, tokenValidationParameters, out validatedToken);
Console.WriteLine($"{claimsPrincipal.Identity.Name}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
RESULT
IDX10511: Signature validation failed. Keys tried:
'System.Text.StringBuilder'. kid: 'System.String'. Exceptions
caught: 'System.Text.StringBuilder'. token:
'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
I also tried both attempts without the "-----BEGIN CERTIFICATE-----\n" and "\n-----END CERTIFICATE-----" additions to the signing key and had the same results as well as trying with ASCII encoding instead of UTF8.
==== UPDATE ====
I was able to use the Microsoft.IdentityModel.Protocols and Microsoft.IdentityModel.Protocols.OpenIdConnect libraries to validate the token using the following code to create the signing keys:
var issuer = jwtToken.Issuer;
var audiences = jwtToken.Audiences;
var issuerOpenIdUri = $"{issuer}/.well-known/openid-configuration";
string stsDiscoveryEndpoint = issuerOpenIdUri;
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = configManager.GetConfigurationAsync().Result;
...and then use the OpenIdConnectConfiguration to assign the keys to the TokenValidatetionParameters:
var tokenValidationParameters = new TokenValidationParameters()
{
ValidAudiences = audiences,
ValidIssuer = issuer,
IssuerSigningKeys = config.SigningKeys,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true
};
However I am still curious why my original attempts would not work. I would like to understand a little better what is happening under the hood.
Microsoft.IdentityModel.Tokens.X509SecurityKey uses the certificate thumbprint in base 16 in its KeyId. Azure Active Directory KeyId uses the certificate thumbprint in base 64.
To use same key ID format as that used by Azure Active directory ,It will be possible (Just the way you did) with Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever and by using signing keys this way
myTokenValidationParameters.IssuerSigningKeys = myOpenIdConnectionConfiguration.SigningKeys;
The error may also occur when “aud” claim doesn’t match the audience which is nothing but the client Id and can be mitigated by giving proper scopes in AAD and in code with proper audience and scope and by granting consent.
I'm using Auth0 and parsing its idToken server-side like this:
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(idToken); // idToken comes from client using auth0.js
var sub = jwtToken.Claims.First(claim => claim.Type == "sub").Value;
The above code works well and I'm able to parse the idToken successfully, but I'd like to validate the idToken before trusting it, so I've tried this:
string clientSecret = "{client_secret}"; // comes from Auth0 application's client secret
var validations = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "some value", // used "iss" from here: https://jwt.io/
ValidAudience = "some value", // used "aud" from here: https://jwt.io/
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(clientSecret)),
};
var principal = tokenHandler.ValidateToken(idToken, validations, out var validatedToken);
When trying to validate the token, it results in this exception:
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException
HResult=0x80131500
Message=IDX10501: Signature validation failed. Unable to match key:
kid: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'
I grabbed the issuer and audience values by parsing one of the tokens here: https://jwt.io/. The client secret is my application's client secret at Auth0.
How can I validate Auth0's idToken using JwtSecurityTokenHandler?
To manually validate Auth0's JWT token, you need these 2 Nuget packages:
System.IdentityModel.Tokens.Jwt
Microsoft.IdentityModel.Protocols.OpenIdConnect
Then get these values from Auth0's application settings
string auth0Domain = ""; // Note: if your Domain is foo.auth0.com, this needs to be https://foo.auth0.com/
string auth0ClientId = "";
Validate Auth0's token as follows:
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{auth0Domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
var validations = new TokenValidationParameters
{
ValidIssuer = auth0Domain,
ValidAudiences = new[] { auth0ClientId },
IssuerSigningKeys = openIdConfig.SigningKeys
};
var user = tokenHandler.ValidateToken(idToken, validations, out var validatedToken);
With the validated token, you can extract useful info like this:
var securityToken = (JwtSecurityToken)validatedToken;
var userId = user.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
Source: Manually validating a JWT using .NET
Credit: #Nkosi
I am struggling with the implementation (or the understanding) of signing keys for JWT Bearer Token authentication. And I hope somebody can help me or explain me what I am missunderstanding.
The last few weeks I crawled tons of tutorials and managed to get a custom Auth-Controller running which issues my tokens and managed to set up the JWT bearer authentication to validate the tokens in the header.
It works.
My problem is that all examples and tutorials either generate random or inmemory (issuer) signing keys or use hardcoded "password" strings or take them from some config file (look for "password" in the code samples).
What I mean for validation setup (in the StartUp.cs):
//using hardcoded "password"
SecurityKey key = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes("password"));
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "MyIssuer",
ValidateAudience = true,
ValidAudience = "MyAudience",
ValidateLifetime = true,
IssuerSigningKey = key
}
});
In the AuthController creating the token:
//using hardcoded password
var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes("password"));
SigningCredentials credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken // Create the JWT and write it to a string
(
issuer: _jwtTokenSettings.Issuer,
audience: _jwtTokenSettings.Audience,
claims: claims,
notBefore: now,
expires: now.AddSeconds(_jwtTokenSettings.LifetimeInSeconds),
signingCredentials: credentials
);
In this question they used:
RSAParameters keyParams = RSAKeyUtils.GetRandomKey();
My (current) assumptions was that in production you should not use hardcoded strings or strings from config files for the token signing keys. But instead use some certificate files??? Am I wrong?
So I tried to replace the strings with a certificate which works in the auth controller:
//using a certificate file
X509Certificate2 cert = new X509Certificate2("MySelfSignedCertificate.pfx", "password");
X509SecurityKey key = new X509SecurityKey(cert);
SigningCredentials credentials = new SigningCredentials(key, "RS256");
var jwt = new JwtSecurityToken // Create the JWT and write it to a string
(
issuer: _jwtTokenSettings.Issuer,
audience: _jwtTokenSettings.Audience,
claims: claims,
notBefore: now,
expires: now.AddSeconds(_jwtTokenSettings.LifetimeInSeconds),
signingCredentials: credentials
);
But there seems no way to get the validation using a certificate.
X509Certificate2 cert = new X509Certificate2("MySelfSignedCertificate.pfx", "password");
SecurityKey key == // ??? how to get the security key from file (not necessarily pfx)
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "MyIssuer",
ValidateAudience = true,
ValidAudience = "MyAudience",
ValidateLifetime = true,
IssuerSigningKey = key
}
});
Am I wrong that I should use certificates for the signing keys?
How else would I change the signing keys in production when the auth controller is on a different server than the consuming/secured web api (may one time, not now)?
It seems I also miss the point (required steps) to get the answer of this question working.
Now I got it running I am still missing the point if it should be like that?
Noteworthy: File not found (after deployment to server)
For all those using this and it works when started from Visual Studio but after deployment to a server / azure it says "File not found":
read and upvote this question: X509 certificate not loading private key file on server
Noteworthy 2: one actually doesn't need it for token based authentication
The public key does not need to be on the API side. It will be retrieved via the discovery endpoint of the authentication server automatically.
Oh dear, that simple:
SecurityKey key = new X509SecurityKey(cert);
Or as complete sample from above:
X509Certificate2 cert = new X509Certificate2("MySelfSignedCertificate.pfx", "password");
SecurityKey key = new X509SecurityKey(cert); //well, seems to be that simple
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "MyIssuer",
ValidateAudience = true,
ValidAudience = "MyAudience",
ValidateLifetime = true,
IssuerSigningKey = key
}
});
A very important point, if you are using certificate files, that while the server requires the file with the private key, the client should only use the public key.
You do not want to be giving your private key file to anyone; they only ever need the public key.
// On client
var publicCert = new X509Certificate2("MySelfSignedCertificate.cer");
var publicKey = new X509SecurityKey(publicCert);
...
IssuerSigningKey = publicKey
The simplest way to convert the PFX (private) to CER (public) may be to import into the Windows certificate manager, then export with the public key only.
From the command line, you can create also use PowerShell 5 (not yet in PowerShell 6):
Get-PfxCertificate -FilePath MySelfSignedCertificate.pfx | Export-Certificate -FilePath MySelfSignedCertificate.cer
Alternatively, you can install and use OpenSSL to convert it from the command line.
Note 1: As you found, once you set the Authority, the auto-discovery may be able to find the public key from the server.
Note 2: Rather than store the certificate in a file, you can also store it in the Windows certificate store, and reference it by thumbprint (both PFX and CER files can be imported).
I have IdentityServer4 that generates signed JWT tokens.
In my web api I added auth middleware to validate these tokens:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = env.IsProduction() ? "https://www.example.com/api/" : "http://localhost/api/",
AllowedScopes = { "WebAPI", "firm",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile },
RequireHttpsMetadata = env.IsProduction(),
});
It works perfectly. However, I suspect it doesn't verify signature of jwt token because there is no public key configured to validate token. How to configure token signature validation?
PS: I try to use UseJwtBearerAuthentication instead this way:
var cert = new X509Certificate2("X509.pfx", "mypassword");
var TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidIssuer = env.IsProduction() ? "https://www.example.com/api/" : "http://localhost/api/",
IssuerSigningKey = new X509SecurityKey(cert),
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Authority = env.IsProduction() ? "https://www.wigwam3d.com/api/" : "http://localhost/api/",
Audience = "WebAPI",
RequireHttpsMetadata = env.IsProduction(),
TokenValidationParameters = TokenValidationParameters
});
It also works (and I hope validates token signature also!) but gives me another bug:
UserManager.GetUserAsync(HttpContext.HttpContext.User)
return null, while using UseIdentityServerAuthentication returns me correct User
I think there is no need to add certificate to you API for validation. .UseIdentityServerAuthentication() middleware calls your IdentiyServer to retrieve public key on startup from https://www.example.com/api/.well-known/openid-configuration. At least that's my understanding how it works.
Finally I done it with JwtBearerAuthentication,
GetUserAsync function failure can be fixed with call to:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
because of this issue: https://github.com/aspnet/Security/issues/1043
Any ideas to configure the same using IdentityServer auth are welcome!