I have a specific use-case where I need to JSON serialize a payload class, encrypt, pass along via handshakes between multiple application APIs in disparate domains, and each receiver must validate and decrypt the serialized class for rehydration and usage within the APIs. Leveraging the foundation and features of JWTs was a logical choice as the carrier for this payload. JWE was selected to gain confidentiality in the JWT’s payload.
Keys will be generated with legitimate certs for actual production implementation. Existing software infra is mostly all MS dotnet core.
I’m struggling specifically with the usage of the dotnet 8 Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor class and the JsonWebTokenHandler.CreateToken method as well as the Decrypt method. Details below:
Specific question:
How do you select the proper CreateToken method? Looking at the docs, it has 13 different signatures, which implies the developers wanted to support a wide array of implementations. MS’s documentation here: JsonWebTokenHandler.CreateToken Method I must be grossly misinterpreting something to not understand this. I need to supply a serialized payload string, but I also need to supply SecurityDescriptor data for the receiver to validate. None of the 13 signatures allow for both, so I must be going about this incorrectly.
Specific Issue To Address #1:
It appears that to use validation in JsonWebTokenHandler.ValidateTokenAsync() on the JWE receiving side of the application/API app, you must supply data for the Issuer and Audience and they must match that same data that was used during token creation. The IssueAt and Expires datetime stamps must be correct as well (e.g. not out of range at reception and validation time.)
Current setup
JWE creation app set up to use RSA private key for encryption read from PEM file and an ECC key for signing, read from a PEM file and both were generated by openssl:
string rsaKeys;
using (var streamReader = System.IO.File.OpenText(@"rsa-crypt.pem"))
rsaKeys = streamReader.ReadToEnd();
string eccSigningKey;
using (var streamReader = System.IO.File.OpenText(@"ecc-crypt.pem"))
eccSigningKey = streamReader.ReadToEnd();
var privateEncKeyId = "somerandomid1234";
var privateKey = RSA.Create();
privateKey.ImportFromPem(rsaKeys.ToCharArray());
var privateSigningKeyId = "somerandomid5678";
var privateSignKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
privateSignKey.ImportFromPem(eccSigningKey.ToCharArray());
var privateEncryptionKey = new RsaSecurityKey(privateKey) { KeyId = privateEncKeyId };
var publicEncryptionKey = new RsaSecurityKey(privateKey.ExportParameters(false)) {
KeyId = privateEncKeyId };
var privateSigningKey = new ECDsaSecurityKey(privateSignKey) { KeyId =
privateSigningKeyId };
var publicSigningKey = new
ECDsaSecurityKey(ECDsa.Create(privateSignKey.ExportParameters(false))) { KeyId =
privateSigningKeyId };
var signingCredentials = new SigningCredentials(
privateSigningKey, SecurityAlgorithms.EcdsaSha256);
var encryptingCredentials = new EncryptingCredentials(
publicEncryptionKey, SecurityAlgorithms.RsaOAEP,
SecurityAlgorithms.Aes256CbcHmacSha512);
var mySerializedClassPayload = "{"id":1,"phone":"1112223333"}";
At this point I can use the following CreateToken method with this signature
var handler = new JsonWebTokenHandler();
handler.SetDefaultTimesOnTokenCreation = true;
handler.TokenLifetimeInMinutes = 1;
var token = handler.CreateToken(mySerializedClassPayload,
signingCredentials,encryptingCredentials,CompressionAlgorithms.Deflate);
Now I can attempt a validation operation
var result = decryptHandler.ValidateTokenAsync(token,
new TokenValidationParameters{
IssuerSigningKey = publicSigningKey,
TokenDecryptionKey = privateEncryptionKey}
);
Even though I setDefaultTimesOnTokenCreation and TokenlifetimeInMinutes, the validation result shows IsValid=false with an error:
IDX10225: Lifetime validation failed. The token is missing an Expiration Time. Tokentype: ‘Microsoft.IdentityModel.JsonWebTokens.JsonWebToken’.”} System.Exception {Microsoft.IdentityModel.Tokens.SecurityTokenNoExpirationException
Ok, I can accept this for the moment, all good. Lets move on to the better example using “proper” JWT data which I know will pacify the Validation method in the example below.
Specific Issue to Address #2:
If I switch over to using the SecurityTokenDescriptor class, I have a different issue. Where do I provide my payload when using a SecurityTokenDescriptor? I can satisfy the Issuer, Audience, time expiry, and use claims (both outer and inner) for validation purposes, but I don’t seem to understand how to use this technique with a custom payload. For example, if I instantiate this way, and randomly toss the serialized class into the inner claims dictionary since it is in fact encrypted here, just as a sloppy experiment:
var descriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor {
Issuer = "test-issuer",
Audience = "https://test.com",
IssuedAt = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddMinutes(1),
AdditionalHeaderClaims = new Dictionary<string, object> { { "test-outer-header-
claim", "abc123456" } },
AdditionalInnerHeaderClaims = new Dictionary<string, object> { { "serializedclass",
mySerializedClassPayload } },
EncryptingCredentials = encryptingCredentials,
SigningCredentials = signingCredentials
};
var secondTestHandler = new JsonWebTokenHandler();
var secondTestToken = secondTestHandler.CreateToken(descriptor); //<-- CreateToken with descriptor AND a payload?
The validation here works OK since we used the SecurityDescriptor class, using this:
var validationHandler = new JsonWebTokenHandler();
var theToken = new JsonWebToken(secondTestToken);
var validationResult = validationHandler.ValidateTokenAsync(
theToken,
new TokenValidationParameters{
ValidIssuer = "test-issuer",
ValidAudience = "https://test.com",
IssuerSigningKey = publicSigningKey,
TokenDecryptionKey = privateEncryptionKey}
);
The Final Big Question:
How does one get the payload in the “right” region of the JWT? Sticking it in the inner claims collection couldn’t possibly be the right path. BUT, amazingly, in both of these examples #1 and #2, I’m able to decrypt the JWT and in both examples, actually see the serialized payload. In the first example, using this signature for CreateToken:
handler.CreateToken(mySerializedClassPayload,
signingCredentials,encryptingCredentials,CompressionAlgorithms.Deflate);
I’m able to see in the decryption result a section in the decoded JWT that says
Header: { "alg": "ES256", "kid": "somerandomid5678", "typ": "JWT" } Payload: { "id":1, "phone":"1112223333" }
which is good. It obviously fails validation due to lack of audience, time, and other headers not being available.
In the second example using this signature for CreateToken:
handler.CreateToken(descriptor);
Yes it passes validation using the TokenValidationParameters above, very excellent, and I’m able to see in the decryption result section the JWT and when decoded it contains:
Header: { "alg": "ES256", "kid": "somerandomid5678", "serializedclass": "{"id":1,"phone":"1112223333"}", "typ": "JWT" } Payload: { "aud": "https://test.com", "iss": "test-issuer", "exp": 1725826344, "iat": 1725826284, "nbf": 1725826284 }
If you made it this far, congratulations, I know I just posted a ton of text. 🙂 I’m more than grateful for any direction or guidance on this.
1