Reputation: 10258
I have found examples of asymmetric signing in .NET FW and examples of symmetric signing in .NET Core, but I cannot figure out how to asymmetrically verify a JWT in .NET Core. Given a URL to a JWK Set or given a public key, how can I verify a token in .NET Core?
Upvotes: 7
Views: 12977
Reputation: 10258
I ended up implementing the OpenID Connect Discovery spec, which allows you to publish the token endpoint and keyset endpoint in a standard format. Then I could use the AddJwtBearer()
AuthenticationBuilder
extension method to automatically cache the keyset, verify tokens, and populate the ClaimsPrincipal
.
To write your own token service that implements the OpenID Connect Discovery protocol, you will need to:
Implement a route /keys
that serves a Microsoft.IdentityModel.Tokens.JsonWebKeySet
object derived from your pfx certificates.
JsonWebKeySet GetJwksFromCertificates(IEnumerable<X509Certificate2> certificates)
{
var jwks = new JsonWebKeySet();
foreach (var certificate in certificates)
{
var rsaParameters = ((RSA)certificate.PublicKey.Key).ExportParameters(false);
var jwk = new JsonWebKey
{
// https://tools.ietf.org/html/rfc7517#section-4
Kty = certificate.PublicKey.Key.KeyExchangeAlgorithm,
Use = "sig",
Kid = certificate.Thumbprint,
X5t = certificate.Thumbprint,
// https://tools.ietf.org/html/rfc7517#appendix-B
N = Convert.ToBase64String(rsaParameters.Modulus),
E = Convert.ToBase64String(rsaParameters.Exponent),
};
jwks.Keys.Add(jwk);
}
return jwks;
}
/not-yet-implemented
that returns 501 Not Implemented
./.well-known/openid-configuration
that serves a Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration
object.
OpenIdConnectConfiguration GetOpenIdConnectConfiguration(string issuer) {
var configuration = new OpenIdConnectConfiguration
{
Issuer = issuer,
TokenEndpoint = issuer + "/token",
AuthorizationEndpoint = issuer + "/not-yet-implemented",
JwksUri = issuer + "/keys",
};
configuration.GrantTypesSupported.Add(grantType);
return configuration;
}
Implement a route /token
that uses your application-specific logic to authenticate the user and generate a ClaimsIdentity
, then creates a System.IdentityModel.Tokens.Jwt.JwtSecurityToken
using a JwtSecurityTokenHandler
.
JwtSecurityToken CreateJwt(
string issuer,
TimeSpan lifetime,
ClaimsIdentity claimsIdentity,
X509Certificate2 signingCertificate)
{
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = issuer,
Expires = DateTime.UtcNow.Add(lifetime),
NotBefore = DateTime.UtcNow,
Subject = claimsIdentity,
SigningCredentials = new X509SigningCredentials(signingCertificate),
};
return new JwtSecurityTokenHandler().CreateJwtSecurityToken(tokenDescriptor);
}
I would also encourage you to implement the OAuth client_credentials
grant flow for your /token
route.
I published a full writeup of this: non-paywalled link.
Upvotes: 4
Reputation: 25380
The only difference between ASymmetric Signing & Symmetric Signing is the signing keys. Just construct a new ASymmetric Security Key to token validation parameters will make it.
Suppose you want to use the RSA algo. Let's use powershell to export a pair of RSA keys as below:
$rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider -ArgumentList 2048
$rsa.ToXmlString($true) | Out-File key.private.xml
$rsa.ToXmlString($false) | Out-File key.public.xml
Now we'll use the two keys to sign the token.
Since the rsa.FromXmlString()
api is support by .NET Core, I just copy @myloveCc's code to construct a RsaParameters
in C# (this work is done by the following ParseXmlString()
method):
public static class KeyHelper
{
public static RSAParameters ParseXmlString( string xml){
RSAParameters parameters = new RSAParameters();
System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument();
xmlDoc.LoadXml(xml);
if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue"))
{
foreach (System.Xml.XmlNode node in xmlDoc.DocumentElement.ChildNodes)
{
switch (node.Name)
{
case "Modulus": parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
case "Exponent": parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
case "P": parameters.P = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
case "Q": parameters.Q = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
case "DP": parameters.DP = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
case "DQ": parameters.DQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
case "InverseQ": parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
case "D": parameters.D = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
}
}
}
else
{
throw new Exception("Invalid XML RSA key.");
}
return parameters;
}
public static RsaSecurityKey BuildRsaSigningKey(string xml){
var parameters = ParseXmlString(xml);
var rsaProvider = new RSACryptoServiceProvider(2048);
rsaProvider.ImportParameters(parameters);
var key = new RsaSecurityKey(rsaProvider);
return key;
}
}
Here I add a BuildRsaSigningKey()
helper method to generate a SecurityKey
.
Here's a demo to generate a token with RSA :
public string GenerateToken(DateTime expiry)
{
var tokenHandler = new JwtSecurityTokenHandler();
var Identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "..."),
// ... other claims
});
var xml = "<RSAKeyValue> load...from..local...files...</RSAKeyValue>";
SecurityKey key = KeyHelper.BuildRsaSigningKey(xml);
var Token = new JwtSecurityToken
(
issuer: "test",
audience: "test-app",
claims: Identity.Claims,
notBefore: DateTime.UtcNow,
expires: expiry,
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest)
);
var TokenString = tokenHandler.WriteToken(Token);
return TokenString;
}
To validate it automatically, configure the JWT Bearer authentication as below :
Services.AddAuthentication(A =>
{
A.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
A.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(O =>
{
var xml = "<RSAKeyValue> load...from..local...files...</RSAKeyValue>";
var key = KeyHelper.BuildRsaSigningKey(xml);
O.RequireHttpsMetadata = false;
O.SaveToken = true;
O.IncludeErrorDetails = true;
O.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = key,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
// ... other settings
};
});
If you would like to manually validate it :
public IActionResult ValidateTokenManually(string jwt)
{
var xml = "<RSAKeyValue>... the keys ...</RSAKeyValue>";
SecurityKey key = KeyHelper.BuildRsaSigningKey(xml);
var validationParameters = new TokenValidationParameters
{
IssuerSigningKey = key,
RequireSignedTokens = true,
RequireExpirationTime = true,
ValidateLifetime = true,
// ... other settings
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(jwt, validationParameters, out var rawValidatedToken);
var securityToken = (JwtSecurityToken)rawValidatedToken;
return Ok(principal);
}
Upvotes: 16