Decrypting JSONWebToken Using JSONWebKey or JSONWebKeySet in C#

Viewed 2215

I have been around and around on the Internet trying various solutions and all of them have broken in various ways. I am writing in hopes that either someone on StackOverflow familiar with a similar workflow or someone from the Microsoft.IdentityModel.JsonWebTokens team can step in to help.

What I am trying to do is to decrypt an OAuth Access token encrypted by a JSONWebKey (JWK) so that I can read the claims data. Right now I am stuck on the decryption step. I am using C# on OS X with Visual Studio for Mac. Among the approaches I have tried are using the older JWT library and trying to create all sorts of RSA objects via round-about ways. However, what I would LIKE to do is something like the following:

using System;
using System.Linq;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
using Microsoft.AspNetCore.WebUtilities;

namespace jwtdecoder {
    class Program {
        static void Main(string[] args) {
            
            /* Exact string of a token retrieved from the IDP/OAuth server. */
            String token = "BASE64EncodedTokenGoesHere";

            /* Trust me that we have a valid JWKS string here that has all the properties mentioned in the RSA params below 
             * and uses RSA OAEP 256 */
            String jwks = "JWKSSTRINGGOESHERE";


            /* Get our basic objects from the Strings above. */
            JsonWebKeySet exampleJWKS = new JsonWebKeySet(jwks);
            JsonWebKey exampleJWK = exampleJWKS.Keys.First();
            JsonWebToken exampleJWT = new JsonWebToken(token);

            /* Create RSA from Elements in JWK */
            RSAParameters rsap = new RSAParameters{
                Modulus = WebEncoders.Base64UrlDecode(exampleJWK.N),
                Exponent = WebEncoders.Base64UrlDecode(exampleJWK.E),
                D = WebEncoders.Base64UrlDecode(exampleJWK.D),
                P = WebEncoders.Base64UrlDecode(exampleJWK.P),
                Q = WebEncoders.Base64UrlDecode(exampleJWK.Q),
                DP = WebEncoders.Base64UrlDecode(exampleJWK.DP),
                DQ = WebEncoders.Base64UrlDecode(exampleJWK.DQ),
                InverseQ = WebEncoders.Base64UrlDecode(exampleJWK.QI)
            };
            RSA rsa = RSA.Create();
            rsa.ImportParameters(rsap);
            RsaSecurityKey rsakey = new RsaSecurityKey(rsa);

            /* Create a JSON Token Handler and Try Decrypting the Token */
            JsonWebTokenHandler exampleHandler = new JsonWebTokenHandler();
            TokenValidationParameters validationParameters = new TokenValidationParameters {
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = false, /* Have also tried with this set to True */
                TokenDecryptionKey = rsakey
            };

            String clearToken = exampleHandler.DecryptToken(exampleJWT, validationParameters);
            /* The line above results in an error
             * "IDX10609: Decryption failed. No Keys tried: token: 'System.String'."
             */
        }
    }
}

My IDP/OAuth Server is Micro Focus / NetIQ Access Manager using a JSON Web Key that I generated outside of it (and imported as a new Resource Server). However, I do not think that detail has much to do with the problem. When I create the JSONWebKey and JSONWebToken objects in C# I see the properties I expect to see and I get no exceptions. I can also decode (not decrypt) the token string with non-C# tools and see the header properties etc. that I expect.

Specifically, for the exampleJWT object in the code above the header is as follows:

{{ "alg": "RSA-OAEP-256", "enc": "A128CBC-HS256", "typ": "JWT", "cty": "JWT", "zip": "DEF", "kid": "5BbVY7F77gz9LWE4tUjXwNFt9qhINvWBR7Pkm1ZJlEA" }}

The exampleJWT object also has the following properties with values that look either encoded or encrypted: EncodedHeader, EncodedToken, EncryptedKey, CipherText, and AuthenticationTag There is no plaintext claims data or date data in the object at present.

My thinking is that I have not set up my code correctly for the Decrypt method to apply the JSONWebToken RSA information properly so that the token can be turned into clear text. However, as I say, I tried a bunch of different methods using other C# classes and approaches and none of them has been any more successful. I would love to know what I am fundamentally misunderstanding about this process. Thank you in advance.

To answer Michal's question below: Four dots (when base-64 encoded), so five sections.

2 Answers

I ran into this issue as well about 3 weeks ago.

As you've already found out, RSA-OAEP-256 is currently not supported by IdentityModel. Going with RSA_15 till this is implemented wasn't an option for me, but RSA-OAEP-256 is supported by .NET, so I utilized the CryptoProviderFactory to delegate this to my own implementation (which is basically a call to Decrypt).

First you need to implement a class with the ICryptoProvider interface:

public class CryptoProvider : ICryptoProvider
{
    public bool IsSupportedAlgorithm(string algorithm, params object[] args)
    {
      if (algorithm == "RSA-OAEP-256")
        return true;

      return false;
    }

    public object Create(string algorithm, params object[] args)
    {
      if (algorithm.Equals("RSA-OAEP-256"))
        return new RsaOaepKeyWrapProvider(args[0] as SecurityKey, algorithm, (bool)args[1]);

      return null;
    }

    public void Release(object cryptoInstance)
    {
      throw new NotImplementedException();
    }
}

Then you'll need to derive from KeyWrapProvider:

public class RsaOaepKeyWrapProvider : KeyWrapProvider
{
    public RsaOaepKeyWrapProvider(SecurityKey key, string algorithm, bool willUnwrap)
    {
      Key = key;
      Algorithm = algorithm;
    }

    protected override void Dispose(bool disposing)
    {
    }

    public override byte[] UnwrapKey(byte[] keyBytes)
    {
      //Get your RSA Object
      var rsa = CryptoConfig.GetRSA();
      return rsa.Decrypt(keyBytes, RSAEncryptionPadding.OaepSHA256);
    }

    //We don't need Key Wrapping
    public override byte[] WrapKey(byte[] keyBytes)
    {
      throw new NotImplementedException();
    }

    public override string Algorithm { get; }
    public override string Context { get; set; }
    public override SecurityKey Key { get; }
}

And finally you need to set rsakey.CryptoProviderFactory to an instance of your CryptoProvider class.

In addition to the fact that not all of the KeyWrapProviders are supported, IdentityModel.Tokens also lacks support for Authenticated Encryption with AES-GCM. With some tweaks to the above implementation you can support this too. When this finally gets implemented in the library you can just delete the additional code without needing to change other implementations.

Thank you Michal for confirming that what I was doing should have worked, and spurring me to debug the JSONWebToken Nuget package. Since there is no debug info included in the NuGet package I built Microsoft.IdentityModel.JsonWebTokens from source and stepping through that led to the answers.

There were two issues, both fatal.

  1. The JWT decryption key ID needs to match the provided decryption key ID, and I was not copying the KID from the JWKS to the RSA object. While I was fixing this I also copied the key size, although the default would have worked. My example code needed to be changed as follows:

    /* Existing code. */
    RSA rsa = RSA.Create();
    rsa.ImportParameters(rsap);
    RsaSecurityKey rsakey = new RsaSecurityKey(rsa);
    /* New stuff here. */
    rsakey.KeyId = exampleJWK.KeyId;
    rsa.KeySize = exampleJWK.KeySize;
    
  2. The second issue should be cleared up in a few months in a future release of the ADAL libraries that include the Microsoft.IdentityModel.JsonWebTokens namespace. Although dotnet generally supports RSA-OAEP-256 across all platforms, it is not yet supported for ADAL's JSONWebToken implementation.
    Cross-Platform Support Note:
    https://docs.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography
    GitHub Issue Regarding ADAL and RSA-OAEP-256:
    https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1293

I generated and installed a new JWKS using RSA_15, grabbed a new OAuth JWT from Access Manager, and my code worked. At the end I got a string back that I could throw into a new JSONWebToken object and all fields (claims, dates, etc. etc.) were present and correct.

For those of you wondering what the point of all this was, I really like C# and Microsoft's newer libraries but prefer to develop on a Mac and deploy to Linux. I really like NAM (Access Manager) as an on-prem SAML/OAuth solution. But I want to decrypt tokens within the resource server / application using them rather than sending them back to NAM for verification and decryption (faster and less moving parts). So now I can do that. Once the libraries support RSA-OAEP-256 I will replace the JWKS for better security.

Finally, to answer Michal's final question, per the GitHub link above the System.string reference in the exception is part of the current implementation but apparently should not be.

Related