Encrypting your JSON Web Tokens with JSON Web Encryption allows you to protect your tokens from the prying eyes of the browser and 3rd party systems. They provide integrity, authentication, and confidentiality. Combined with JSON Web Signing (JWS), they can also provide non-repudiation, making them usable with OAuth and OpenID Connect.
In this article, you’ll learn how to create and validate an encrypted JWT in .NET using JWT best practices, Nested JWTs, and Microsoft’s IdentityModel libraries.
JWE tokens in .NET
To use encrypted JWTs in .NET, you’ll need to bring in the JWT library from Microsoft.IdentityModel:
dotnet add package Microsoft.IdentityModel.JsonWebTokens
The Microsoft.IdentityModel.JsonWebTokens library includes the modern APIs for JWT creation, and I recommend its use over the older APIs found in System.IdentityModel.Tokens.Jwt. Other JWT libraries are available, but this is the most widespread.
Preparing signing and encryption keys
To create these samples, I created two keys, an ECC key for signing and an RSA key for encryption. I’ve also assigned both random key IDs.
var encryptionKey = RSA.Create(3072); // public key for encryption, private key for decryption
var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); // private key for signing, public key for validation
var encryptionKid = "8524e3e6674e494f85c5c775dcd602c5";
var signingKid = "29b4adf8bcc941dc8ce40a6d0227b6d3";
var privateEncryptionKey = new RsaSecurityKey(encryptionKey) {KeyId = encryptionKid};
var publicEncryptionKey = new RsaSecurityKey(encryptionKey.ExportParameters(false)) {KeyId = encryptionKid};
var privateSigningKey = new ECDsaSecurityKey(signingKey) {KeyId = signingKid};
var publicSigningKey = new ECDsaSecurityKey(ECDsa.Create(signingKey.ExportParameters(false))) {KeyId = signingKid};
In a real-life scenario, the private keys would likely be loaded from a vault somewhere and the public keys parsed from something like a JSON Web Key, like the following example, that has a use of “enc”:
{
"kty": "RSA",
"use": "enc",
"kid": "8524e3e6674e494f85c5c775dcd602c5",
"n": "2xdqc8EjvmNAv9oejLiSm4aI0w38h-8bUfNC8N7qOJEs9VhxU4VjGpld3lNhg31Pg3G7v76m4cwfiUEzKPCQb3c8AzBbt76WjZthQIqg9mdVVN1WAnlaZzlkpncKDGlRpm824abGF8FsaP29ehznueO-Uw1uOVcKG2VKCulmwiohDQtTH_of_nis4zY5KukO0PN7SVkr2eS5Z_zXoZgXqjyDv3H4JuBDdD1LJbl07fdQ9Vf4vkYD-rbqPuOfpPDjytKVWVgbU0_0z8669UYvBuWDhOtbPyK2NkLIJp6r_vfsV8OoaZlVI5oMZlLoNJTYRBBVymskfxpAlVmKZfHD35KUan9cOikr9lszQ-PutgYPLXEf_mOBK25tbkkwtghDKZEVw5RSMv-VKJWInkTHdF6SJsQ5KvQvHHpTzsrGNXW36BuVMEXayEzel55V84SYZzs_-nlssOjBKfjzCNTIdRdhwnOTEotcxSaQNp8L0fTfMJMb9bbqSl_PCIoMReIx",
"e": "AQAB"
}
These could just as easily be keys that are part of an X.509 certificate.
Creating JWE tokens in .NET
To create an encrypted JWT in .NET, you can use the CreateToken method in JsonWebTokenHandler
.
This will use the issuer’s private key for signing and the recipient’s public key for encryption.
var handler = new JsonWebTokenHandler();
string token = handler.CreateToken(new SecurityTokenDescriptor
{
Audience = "api1",
Issuer = "https://idp.example.com",
Claims = new Dictionary<string, object> { { "sub", "811e790749a24d8a8f766e1a44dca28a" } },
// private key for signing
SigningCredentials = new SigningCredentials(
privateSigningKey, SecurityAlgorithms.EcdsaSha256),
// public key for encryption
EncryptingCredentials = new EncryptingCredentials(
publicEncryptionKey, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)
});
This is using RSA-OAEP as the wrapping algorithm and A256CBC-HS512 as the encryption algorithm. A256GCM (AES-GCM) would be a better choice for authenticated encryption, but unfortunately, the .NET JWT libraries only support AES-CBC. The signing algorithm is ES256. Again, better JWT algorithms are available.
The IdentityModel library will always create a JWE containing a Nested JWT. This means that your JWT is signed and then encrypted, ensuring that your token will have the confidentiality of JWE and the non-repudiation of JWS. You can learn more about this in my article “Understanding JWE”.
This produces a JWE with the following header, showing that the content behind the encrypted ciphertext (cty) is a JWT.
{
"alg": "RSA-OAEP",
"enc": "A256CBC-HS512",
"kid": "8524e3e6674e494f85c5c775dcd602c5",
"typ": "JWT",
"cty": "JWT"
}
Decrypting and validating JWE Tokens in .NET
To decrypt the token and validate the Nested JWT, you use the ValidateToken
method in JsonWebTokenHandler
.
This validation process includes the usual issuer & audience checks on the Nested JWT and requires the issuer’s public key for signature validation and the recipient’s private key for decryption.
var handler = new JsonWebTokenHandler();
TokenValidationResult result = handler.ValidateToken(
token,
new TokenValidationParameters
{
ValidAudience = "api1",
ValidIssuer = "https://idp.example.com",
// public key for signing
IssuerSigningKey = publicSigningKey,
// private key for encryption
TokenDecryptionKey = privateEncryptionKey
});
This results in a TokenValidationResult
that includes an IsValid
flag, any errors, the claims parsed from the inner token, and the parsed SecurityToken
object.
public IDictionary<string, object> Claims { get; }
public ClaimsIdentity ClaimsIdentity { get; }
public Exception Exception { get; }
public string Issuer { get; }
public bool IsValid {get;}
public IDictionary<string, object> PropertyBag { get; }
public SecurityToken SecurityToken { get; }
public CallContext TokenContext { get; }
public string TokenType { get; }
Remember, you must always validate the inner token and should always expect it to be signed (Microsoft’s default behavior).
If your result contains an exception of IDX10504 “Unable to validate signature, token does not have a signature”, you have received an unsigned Nested JWT.
Do not accept this, and do not set RequireSignedTokens
to false.
JWE in ASP.NET Core
You can apply the same logic to ASP.NET Core’s JWT middleware. Let’s see what that looks like.
First up, is the JWT authentication middleware, which you can pull from nuget:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Then, in your authentication registration, you need to set the authority and audience properties like usual, with the added step of setting the token decryption key on the token validation parameters, which needs to be your private key used for decryption.
builder.Services.AddAuthentication("jwt")
.AddJwtBearer("jwt", options =>
{
options.Authority = "https://localhost:5000";
options.Audience = "api1";
options.TokenValidationParameters.TokenDecryptionKey = new RsaSecurityKey(encryptionKey);
});
Don’t forget the call to UseAuthentication
in your pipeline!
Source Code
The above code is available in my GitHub samples repository.