81

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;
Cheeso
  • 189,189
  • 101
  • 473
  • 713
Dmitry Nikolaev
  • 3,803
  • 2
  • 19
  • 23

7 Answers7

71

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);
  }
Dmitry Nikolaev
  • 3,803
  • 2
  • 19
  • 23
  • 1
    Just curious why you are choosing to use the low-level constructs here rather than `System.IdentityModel.Tokens.Jwt`? – explunit Dec 23 '15 at 14:00
  • 12
    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. – Dmitry Nikolaev Dec 23 '15 at 20:28
  • 1
    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'. – Dmitry Nikolaev Dec 23 '15 at 20:29
  • 1
    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. – Rob Van Pamel Dec 05 '16 at 18:35
  • 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? – tsiro Mar 30 '20 at 15:01
  • Use `WebEncoders.Base64UrlDecode` instead of rolling your own `FromBase64Url` method. – andleer Aug 10 '20 at 20:25
  • how can the claims can be returned from this? – andreasperelli May 09 '23 at 15:43
40

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;
IamK
  • 2,753
  • 5
  • 30
  • 39
olaf
  • 531
  • 4
  • 4
  • 1
    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: https://jwt.io/#debugger-io Would be nice to know what to set on these RSAParameters to get a live example working, but thanks mate! – Nicholas Petersen Sep 23 '20 at 00:03
  • 1
    @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. – Glen Carpenter Nov 04 '21 at 13:15
24

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

NavidM
  • 1,515
  • 1
  • 16
  • 27
  • 2
    I've been searching for this solution for 2 days. Thank you very much @NvMat – Reath Oct 11 '18 at 19:52
  • Great answer @NvMat. I used your approach for the AddJwtBearer() auth flow in a net core web api. – t2t Jan 23 '19 at 09:14
  • 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](https://stackoverflow.com/q/5927791/1178314). – Frédéric Apr 25 '19 at 13:32
  • 2
    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 – Mikhail Kh Dec 14 '21 at 12:10
  • 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. – Nurkartiko Jul 04 '22 at 09:15
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.

t2t
  • 1,101
  • 2
  • 12
  • 15
  • 2
    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". – Prateek Kumar Dalbehera Jan 16 '20 at 10:37
  • 1
    Any possible solution within .net core 3.1 with TokenValidationParameters without using bouncy castle. – Kamran Shahid Feb 19 '20 at 06:14
2

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
}
jtate
  • 2,612
  • 7
  • 25
  • 35
  • When I try this I get "cannot convert from 'System.Security.Cryptography.RSA' to 'System.Security.Cryptography.X509Certificates.X509Certificate2' on this: new RS256Algorithm(rsaKey) – HitLikeAHammer Jun 02 '22 at 23:46
1

.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.

Jason Williams
  • 2,740
  • 28
  • 36
  • 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! – Mister Q Nov 24 '20 at 08:59
  • 1
    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. – Jason Williams Nov 25 '20 at 14:37
1

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
}
ZOXEXIVO
  • 920
  • 9
  • 21