88

Ok, I understand that the question I am asking may be pretty obvious, but unfortunately I lack the knowledge on this subject and this task seems to be quite tricky for me.

I have an id token (JWT) returned by OpenID Connect Provider. Here it is:

eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ

Its header and payload are decoded as this:

{
 "kid":"1e9gdk7",
 "alg":"RS256"
}.
{
 "iss": "http://server.example.com",
 "sub": "248289761001",
 "aud": "s6BhdRkqt3",
 "nonce": "n-0S6_WzA2Mj",
 "exp": 1311281970,
 "iat": 1311280970,
 "c_hash": "LDktKdoQak3Pk0cnXxCltA"
}

From the OIDC provider's discovery, I've got the public key (JWK):

{
 "kty":"RSA",
 "kid":"1e9gdk7",
 "n":"w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ",
 "e":"AQAB"
}

So, the question is how exactly in C# can I verify this JWT using the public key for the RS256 algorithm I've got? It would be awesome if there is a good tutorial describing this procedure explicitly. However, an example of how to do this using System.IdentityModel.Tokens.Jwt will also work fine.

UPDATE: I understand, that I need to do something like the code below, but I have no idea where to get 'key' for calculating SHA256 hash.

  string tokenStr = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
  string[] tokenParts = tokenStr.Split('.');

  RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
  rsa.ImportParameters(
    new RSAParameters() {
      Modulus = FromBase64Url("w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ"),
      Exponent = FromBase64Url("AQAB")
    });

  HMACSHA256 sha = new HMACSHA256(key);
  byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(tokenParts[0] + '.' + tokenParts[1]));
  byte[] signature = rsa.Encrypt(hash, false);
  string strSignature = Base64UrlEncode(signature);
  if (String.Compare(strSignature, tokenParts[2], false) == 0)
    return true;
4
  • Which role are you fulfilling? (Client, Resource Server?) Commented Dec 22, 2015 at 0:34
  • It's a client application. It should work with a custom OIDC provider (do not have control over it) that uses the RS256 signing algorithm. Everything works fine by the moment and the signature verification is the latest obstacle I am facing. Commented Dec 22, 2015 at 6:41
  • 1
    Does this help? msdn.microsoft.com/en-us/library/0h05c7e2(v=vs.110).aspx Commented Dec 22, 2015 at 12:03
  • Absolutely! Thank you jwilleke! Commented Dec 22, 2015 at 19:37

8 Answers 8

76

Thanks to jwilleke, I have got a solution. To verify the RS256 signature of a JWT, it is needed to use the RSAPKCS1SignatureDeformatter class and its VerifySignature method.

Here is the exact code for my sample data:

  string tokenStr = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
  string[] tokenParts = tokenStr.Split('.');

  RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
  rsa.ImportParameters(
    new RSAParameters() {
      Modulus = FromBase64Url("w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ"),
      Exponent = FromBase64Url("AQAB")
    });

  SHA256 sha256 = SHA256.Create();
  byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(tokenParts[0] + '.' + tokenParts[1]));

  RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
  rsaDeformatter.SetHashAlgorithm("SHA256");
  if (rsaDeformatter.VerifySignature(hash, FromBase64Url(tokenParts[2])))
    MessageBox.Show("Signature is verified");

//...
  static byte[] FromBase64Url(string base64Url)
  {
    string padded = base64Url.Length % 4 == 0
        ? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
    string base64 = padded.Replace("_", "/")
                          .Replace("-", "+");
    return Convert.FromBase64String(base64);
  }
Sign up to request clarification or add additional context in comments.

7 Comments

Just curious why you are choosing to use the low-level constructs here rather than System.IdentityModel.Tokens.Jwt?
First of all, documentation for 'System.IdentityModel.Tokens.Jwt' is awful and out-of-date. The current version of this package has other APIs that are different from those documented in MSDN. I tried using it and it worked for me in general, but I was unable to find a way to perform this simple task of token signature verification.
And the second reason is that I will be working with custom OIDC providers and it is possible that I will be unable to get the list of public keys for one of them. In this situation, I will need to skip signature verification that is, I think, impossible with 'System.IdentityModel.Tokens.Jwt'.
When using .net core in a Unix environment, you can not use the RSACryptoServiceProvider. There is an alternative way in the system.security.cryptography.algorithms. You can use rsa straightaway over there.
what about JWT token generation with RSA signing algorith using private key...is RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048); new RsaSecurityKey(rsa.ExportParameters(true));...code sufficient?
|
47

Here is an example using IdentityModel.Tokens.Jwt for validation:

string tokenStr = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";

RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
  new RSAParameters()
  {
      Modulus = FromBase64Url("w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ"),
      Exponent = FromBase64Url("AQAB")
  });

var validationParameters = new TokenValidationParameters
                {
                    RequireExpirationTime = true,
                    RequireSignedTokens = true,
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateLifetime = false,
                    IssuerSigningKey = new RsaSecurityKey(rsa)
                };

SecurityToken validatedSecurityToken = null;
var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(tokenStr, validationParameters, out validatedSecurityToken);
JwtSecurityToken validatedJwt = validatedSecurityToken as JwtSecurityToken;

3 Comments

Is Modulus input string the public key? I got this to work, great job and thank you, using your exact inputs here. But trying it out with an example here when select RS256, not succeeding to get something working: jwt.io/#debugger-io Would be nice to know what to set on these RSAParameters to get a live example working, but thanks mate!
@NicholasPetersen I know this is a bit late, but the modulus and exponent are derived from the Base64URL encoded public key. Usually they are on the JWK as the "n" and "e" keys. If you do not have a JWK, you will need to derive these values somehow, as the public key string itself will not work.
Excuse my ignorance here, but what would happen if the Jwt does not validate? Would it thrown an error (presumably on the line starting "JwtSecurityToken validatedJwt = "), or should I be comparing validatedJwt with something else to see if it's the same?
27

For anyone that is looking for a quick method to validate RS256 with a public key that has "-----BEGIN PUBLIC KEY-----"/"-----END PUBLIC KEY------"

Here are two methods with the help of BouncyCastle.

    public bool ValidateJasonWebToken(string fullKey, string jwtToken)
    {
        try
        {
            var rs256Token = fullKey.Replace("-----BEGIN PUBLIC KEY-----", "");
            rs256Token = rs256Token.Replace("-----END PUBLIC KEY-----", "");
            rs256Token = rs256Token.Replace("\n", "");

            Validate(jwtToken, rs256Token);
            return true;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            return false;
        }
    }

    private void Validate(string token, string key)
    {
        var keyBytes = Convert.FromBase64String(key); // your key here

        AsymmetricKeyParameter asymmetricKeyParameter = PublicKeyFactory.CreateKey(keyBytes);
        RsaKeyParameters rsaKeyParameters = (RsaKeyParameters)asymmetricKeyParameter;
        RSAParameters rsaParameters = new RSAParameters
        {
            Modulus = rsaKeyParameters.Modulus.ToByteArrayUnsigned(),
            Exponent = rsaKeyParameters.Exponent.ToByteArrayUnsigned()
        };
        using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
        {
            rsa.ImportParameters(rsaParameters);
            var validationParameters = new TokenValidationParameters()
            {
                RequireExpirationTime = false,
                RequireSignedTokens = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                IssuerSigningKey = new RsaSecurityKey(rsa)
            };
            var handler = new JwtSecurityTokenHandler();
            var result = handler.ValidateToken(token, validationParameters, out var validatedToken);
        }
    }

This is a combination of http://codingstill.com/2016/01/verify-jwt-token-signed-with-rs256-using-the-public-key/ and @olaf answer that uses system.IdentityModel.Tokens.Jwt

5 Comments

I've been searching for this solution for 2 days. Thank you very much @NvMat
Great answer @NvMat. I used your approach for the AddJwtBearer() auth flow in a net core web api.
This way of doing things, stripping/decoding manually to a byte array before using BouncyCastle, was failing for me (exception in BouncyCastle). Instead, using BouncyCastle PemReader directly on the key text with ----- delimiters was easier for me and working. I have found this other way in this question.
Thanks! But I got issue with this approach - first time validation success, but second time it catch exception. When I remove using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) and keep just: RSACryptoServiceProvider rsa = new RSACryptoServiceProvider() It start works fine. When I add rsa.Dispose() in the end, I couught same issue
Package 'BouncyCastle 1.8.9' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8' instead of the project target framework 'net6.0'. This package may not be fully compatible with your project.
15

NET Core

To use this in a .NET core web api (.NET Framework see below) in a AddJwtBearer() auth flow I enhanced NvMat's great answer:

Very important is to not use the RSACryptoServiceProvider in an using statement.

    private TokenValidationParameters GetTokenValidationParameters(string key)
    {
        var rs256Token = key.Value.Replace("-----BEGIN PUBLIC KEY-----", "");
        rs256Token = rs256Token.Replace("-----END PUBLIC KEY-----", "");
        rs256Token = rs256Token.Replace("\n", "");

        var keyBytes = Convert.FromBase64String(rs256Token);

        var asymmetricKeyParameter = PublicKeyFactory.CreateKey(keyBytes);
        var rsaKeyParameters = (RsaKeyParameters)asymmetricKeyParameter;
        var rsaParameters = new RSAParameters
        {
            Modulus = rsaKeyParameters.Modulus.ToByteArrayUnsigned(),
            Exponent = rsaKeyParameters.Exponent.ToByteArrayUnsigned()
        };
        var rsa = new RSACryptoServiceProvider();

        rsa.ImportParameters(rsaParameters);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = false,
            RequireSignedTokens = true,
            ValidateAudience = false,
            ValidateIssuer = false,
            IssuerSigningKey = new RsaSecurityKey(rsa),
        };

        return validationParameters;
    }

Then you are able to use authentication in the startup like this:

services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.IncludeErrorDetails = true;
    options.TokenValidationParameters = GetTokenValidationParameters(configuration["Key"]);
    options.Audience = configuration["ClientId"];
});

NET Framework

It is also possible to use this approach in a .NET Framework web api project. All you have to do is add this line to your startup Configure() method:

app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions()
{
     TokenValidationParameters = GetTokenValidationParameters(ConfigurationManager.AppSettings["Key"])
});

One important thing: Make sure you use a verion >=5.0.0 of the JwtSecurityTokenHandler I had problems with the 4.X.X versions.

2 Comments

Thanks for the detailed solution. "Very important is to not use the RSACryptoServiceProvider in an using statement." -- For other readers, if used inside using statement, then .Net Core will complain that "Invalid Token Signature".
Any possible solution within .net core 3.1 with TokenValidationParameters without using bouncy castle.
3

You can do this very easily with Jwt.Net. This function will decode and verify the signature of a JWT and return the payload as a dictionary of claims:

private IDictionary<string, object> Decode(string token, string modulus, string exponent)
{
    var urlEncoder = new JwtBase64UrlEncoder();

    var rsaKey = RSA.Create();
    rsaKey.ImportParameters(new RSAParameters() {
        Modulus = urlEncoder.Decode(modulus),
        Exponent = urlEncoder.Decode(exponent)
    });

    var claims = new JwtBuilder()
        .WithAlgorithm(new RS256Algorithm(rsaKey))
        .MustVerifySignature()
        .Decode<IDictionary<string, object>>(token);

    return claims;
}

Sample use:

string jwt = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
string modulus = "w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ";
string exponent = "AQAB";

try
{
    IDictionary<string, object> claims = Decode(jwt, modulus, exponent);
}
catch (SignatureVerificationException)
{
    // signature invalid, handle it here
}

1 Comment

When I try this I get "cannot convert from 'System.Security.Cryptography.RSA' to 'System.Security.Cryptography.X509Certificates.X509Certificate2' on this: new RS256Algorithm(rsaKey)
2

.NET JWT Signature Verification using System.Security.Cryptography - No 3rd Party DLLs

var errorMessage = string.Empty;

// Google RSA well known Public Key data is available at https://accounts.google.com/.well-known/openid-configuration by navigating to the path described in the "jwks_uri" parameter.
// {
//     e: "AQAB",        // RSA Exponent
//     n: "ya_7gV....",  // RSA Modulus aka Well Known Public Key
//     alg: "RS256"      // RSA Algorithm
// }

var verified = VerifyJWT_RS256_Signature(
    jwt: "oicjwt....", 
    publicKey: "ya_7gV....", 
    exponent: "AQAB",
    errorMessage: out errorMessage);

if (!verified)
{
    // TODO: log error: 
    // TODO: Do something
}

NOTE: The following method verifies OpenID Connect JWT Signatures signed with Asymetric RS256 keys. OpenID Connect providers may opt to use other versions of Asymetric keys or even Symetric keys like HS256. This method does not directly support other key types.

public static bool VerifyJWT_RS256_Signature(string jwt, string publicKey, string exponent, out string errorMessage)
{
    if (string.IsNullOrEmpty(jwt))
    {
        errorMessage = "Error verifying JWT token signature: Javascript Web Token was null or empty.";
        return false;
    }

    var jwtArray = jwt.Split('.');
    if (jwtArray.Length != 3 && jwtArray.Length != 5)
    {
        errorMessage = "Error verifying JWT token signature: Javascript Web Token did not match expected format. Parts count was " + jwtArray.Length + " when it should have been 3 or 5.";
        return false;
    }

    if (string.IsNullOrEmpty(publicKey))
    {
        errorMessage = "Error verifying JWT token signature: Well known RSA Public Key modulus was null or empty.";
        return false;
    }

    if (string.IsNullOrEmpty(exponent))
    {
        errorMessage = "Error verifying JWT token signature: Well known RSA Public Key exponent was null or empty.";
        return false;
    }

    try
    {
        string publicKeyFixed = (publicKey.Length % 4 == 0 ? publicKey : publicKey + "====".Substring(publicKey.Length % 4)).Replace("_", "/").Replace("-", "+");
        var publicKeyBytes = Convert.FromBase64String(publicKeyFixed);

        var jwtSignatureFixed = (jwtArray[2].Length % 4 == 0 ? jwtArray[2] : jwtArray[2] + "====".Substring(jwtArray[2].Length % 4)).Replace("_", "/").Replace("-", "+");
        var jwtSignatureBytes = Convert.FromBase64String(jwtSignatureFixed);

        RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
        rsa.ImportParameters(
            new RSAParameters()
            {
                Modulus = publicKeyBytes,
                Exponent = Convert.FromBase64String(exponent)
            }
        );

        SHA256 sha256 = SHA256.Create();
        byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(jwtArray[0] + '.' + jwtArray[1]));

        RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
        rsaDeformatter.SetHashAlgorithm("SHA256");
        if (!rsaDeformatter.VerifySignature(hash, jwtSignatureBytes))
        {
            errorMessage = "Error verifying JWT token signature: hash did not match expected value.";
            return false;
        }
    }
    catch (Exception ex)
    {
        errorMessage = "Error verifying JWT token signature: " + ex.Message;
        return false;
        //throw ex;
    }

    errorMessage = string.Empty;
    return true;
}

NOTE: Verifying the signature of an OpenID Connect JWT (Javascript Web Token) is only one necessary step of the JWT verification process. Make sure to set a NONCE value which your system can use to prevent Replay attacks. Make sure to validate each parameter of the JWT package for completeness and accuracy.

2 Comments

Hi, your condition ` if (jwtArray.Length != 3 || jwtArray.Length != 5)` is wrong, my token is 3 length long but I hit the error! Cheers for your answer, help me a lot!
Thanks, Mister Q! I checked against my code base and found I had different logic. The correct logic is 'if (jwtArray.Length != 3 && jwtArray.Length != 5)'. I corrected this in the code, above.
2

Try to use JwtUtils nuget package It has pretty simple API:

string publicKey = "@MIIJKgIBAAKCAgEA9GF97STxVGbXpBFmudS/RRT58mfiR/+t2zb4f/uF3qmYb
yuJy2v8xOMbHvMkoKLPLc590zGV88HNvzJHkF5N5HWTB9ZZEWcehf6RcTA==";

if (JWT.RS256.ValidateSignature("{YOUR_JWT_TOKEN}", publicKey))
{
   // Token signature valid
}

Comments

0

If you have multiple public keys like from Google Cloud or Firebase, you can use Microsoft.IdentityModel.JsonWebTokens to validate them all:

  1. (Optionally) download and convert them into SecurityKeys, you should cache them on a server (note that if you use MemoryCache, you need to Dispose them manually when evicted). In the code below, my server needs to validate tokens from both Google Sign-in and Firebase so I merge them together. If you only need public keys from one place, the code would be much simpler.
var http = httpFac.CreateClient();

var certs = await Task.WhenAll(JwtCertUrls.Select(async url =>
{
    var jwks = await http
        .GetFromJsonAsync<Dictionary<string, string>>(url);
    ArgumentNullException.ThrowIfNull(jwks);

    return jwks.Select(cert =>
    {
        var x509Data = Encoding.UTF8.GetBytes(cert.Value);
        var x509Cert = new X509Certificate2(x509Data);

        return new X509SecurityKey(
            x509Cert,
            cert.Key);
    });                    
}));

return certs
    .SelectMany(q => q);
  1. Put them in and validate:
    public async Task<JsonWebToken> ValidateTokenAsync(string token, IEnumerable<SecurityKey> securityKeys)
    {
        var parameters = new TokenValidationParameters()
        {
            IssuerSigningKeys = securityKeys,
        };

        var handler = new JsonWebTokenHandler();

        var result = await handler.ValidateTokenAsync(token, parameters);
        if (result.IsValid)
        {
            return result.SecurityToken as JsonWebToken
                ?? throw new InvalidDataException();
        }
        else
        {
            throw new UnauthorizedAccessException();
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.