Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I verify an asymmetrically signed JWT in dotnet core?

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?

like image 699
twinlakes Avatar asked Jun 21 '19 02:06

twinlakes


People also ask

Is JWT asymmetric?

A JWT can be encrypted using either a symmetric key (shared secret) or asymmetric keys (the private key of a private–public pair). Symmetric key: The same key is used for both encryption (when the JWT is created) and decryption (MobileTogether Server uses the key to verify the JWT).

How do I generate a JWT hs256 key?

Token Generate Mechanism In the API Management, after validating the application key, get the client_secret from variable and use the client_secret to generate the JWT token. For validate the JWT token, the service consumer provides the client_secret and the JWT, of course.


2 Answers

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.

A Little Patching

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.

Token Generation

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;
}

Token validation

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);
}
like image 129
itminus Avatar answered Nov 15 '22 10:11

itminus


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;
    }
    
  • Implement a route /not-yet-implemented that returns 501 Not Implemented.
  • Implement a route /.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.

Update

I published a full writeup of this: non-paywalled link.

like image 27
twinlakes Avatar answered Nov 15 '22 11:11

twinlakes