Friday, 13 October 2017

JWT, JWE, JWS in .Net

JWT in .Net

When I first approached the idea of doing JWT (json web tokens) in .Net, it all seemed a little confusing.

Firstly, it IS confusing because Microsoft have started with a Microsoft.IdentityModel.Tokens namespace, which was eventually migrated into System.IdentityModel.Tokens, deprecating the original namespace but THEN, they added new functionality in System.IdentityModel.Tokens version 5 that references NEW code in Microsoft.IdentityModel.Tokens (which is resurrected). All the usual chaos has started since some things are the same (most class names), some are different. Some code written for v4 of System.IdentityModel.Tokens will not work in version 5. Anyway...

The Basics

Before you can understand how to do this, you should know what json is (JavaScript Object Notation), which is a fairly small way to move data around - much smaller than xml for instance, but it is generally web friendly.

You should also understand the basic concepts of signing and encryption.

Signing using asymmetric key encryption (RSA, DSA etc) allows you to create a packet of data, sign it with your private key and send it to a recipient. Even though the data is NOT private because it is NOT encrypted, the recipient can use your PUBLIC key to verify the signature that you applied to the data, which provides 2 protections (assuming keys are secure etc.) Firstly, it provides integrity of the data. An attacker could not modify the data and leave a valid signature since the private key needed to produce the signature is not available to the attacker. The recipient would know this when they validate the token and should/must discard the data if the signature fails. Secondly. signing provides non-repudiation, which means the sender cannot deny signing the data unless they admit to losing their private keys to an attacker.

It is also possible to sign the data with a symmetrical key, which would be useful if the sender and receiver already have a securely shared secret, which would therefore remove the need to perform asymmetrical signing/verifying, which is computationally expensive. (See Amey's answer here). Obviously this means that either party or anyone who is given this secret can also sign data so is not normally used.

Encryption is about obscuring the real data so an attacker cannot read the data when it is at rest or in transit. With encryption, it is assumed that either there is a securely shared key or that the same key can be derived using something like Diffie-Hellman key exchange.

JWT is an abstract idea that is made concrete in 2 sub-types.

JWS is a form of JWT for signed data, it is not encrypted.

JWE is an encrypted and signed form of JWT.

JWS - Json Web Token Signed

JWS is relatively straight-forward. It is composed of a json header with typ, alg and kid to identify the type ("jwt"), algorithm (signing algorithm, for example "RS256" or a URL for RSA) and the key identifier so the recipient knows which key can be used to verify the signature.

You will need the namespaces:

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography.X509Certificates;

You can create a header either explicitly in .Net or you can allow the helper method CreateSecurityToken to do it for you:

Method 1: Create the JwtHeader yourself (from certificate in this case)

var key = new X509SecurityKey(new X509Certificate2(certFilePath,certPassword));
var algorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";     // RS256
var creds = new SigningCredentials(key, algorithm);
var header = new JwtHeader(creds);

Method 2: Use CreateJwtSecurityToken helper method

var handler = new JwtSecurityTokenHandler();
var token = handler.CreateJwtSecurityToken(issuer, audience, null, creationTime, creationTime.AddSeconds(lifetime), creationTime, creds);
// token now has header automatically

In method 2, the payload is being populated at the same time as the header.

After the header is the payload, which is another json dictionary (a claims list) with some standard claims such as nbf (not before), exp (expiry), iss (issuer) and aud (audience i.e. recipient) as well as any other additional claims that you wish to send. Issuer can be any string that is relevant but if you are using public key discovery, it is useful to use a URL that can be used to lookup .well-known/jwks to see a list of keys related to key ids (kid in the header of the JWT)

If using the first method, you can create the payload in a number of ways but this is probably the easiest:

var payload = new JwtPayload(
    token.Issuer,
    token.Audience,
    null,
    token.CreationTime.UtcDateTime,
    token.CreationTime.AddSeconds(token.Lifetime).UtcDateTime);

payload.AddClaims(token.Claims);
var handler = new JwtSecurityTokenHandler();
var token = new JwtSecurityToken(header, payload);

token is simply an object that contains the data to serialize into JWT.

The second method above already shows how to add the required payload (nbf,exp,iss,aud) in the same call to CreateJwtSecurityToken. Other claims would need to be added afterwards simply by calling token.Payload.AddClaims().

Once these are created, the data is combined and the signature computed across the data using the specified algorithm and key. Once this is done, each of these is base64 encoded and concatenated with a period (.) into 3 blocks. This part is really easy because if you have specified your key correctly, you simply tell the handler to write the token:

var serializedJwt = handler.WriteToken(token);

The result might look something like this:

eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Iiwia2lkIjoiMjVGN0Y2NThDQ0E2NjI2QjRENTdBNEExQTMyOUUyMjBDMjNEQUI3QSIsInR5cCI6IkpXVCJ9
.eyJuYmYiOjE1MDc5MTA0NDAsImV4cCI6MTUwNzkxMDQ3MCwiaXNzIjoicHAiLCJhdWQiOiIxMjM0NTYiLCJteWRhdGEiOiJzb21ldGhpbmdpbmhlcmUifQ
.TNSLgLuj-j9eSC5HiQqS86LLjZ6ZJnGoMAkGMsTqY-pjptQ8qItmrrSE3bf12E5aYHfNr4IWmSdY4-qkQRUXmtb-Ev2c0wE5NkABYqRdAok-AHuBUcRds7VrEQTanB69sKAhtEIZHshLPc4D9IMlYpc2opOzTCBGIB15mX0HodF6hdP6-LeaEeM-rR8v6bnmMsuvzu3GSGOSvPRm_yvZv25ywp4IzEYlbmLcw6NBJt4fx_8ZSEvIQtvCYtMgtAkFbiJ85Lo3sY7m8o8w84ChIG4AgDQi-woRwGU-3RFouppmAgjqPMCgMYn5Tt7Q3rjVtAgulMv0z0tVNvmOVg0zt04sI-CJIkgimAdaNM-O35Lyh4DzPasOXM_HsZ5_3EQoQn1pVRNU_6iBy4X1vGTRNICZon0x3v2MLvQQskaKfbUkSEqs1mceKXgu3cp6GRdT18z1ZUduP5hNYrysCvXtYjmcmmjC-RbnjgNcv3vC51gZvoyQ5OvmtBIvYv_QF14paV1uIxxd8Z_P0z0Z4RrNokUkWwb2n4RYOmW0Ihs7HR-1ba6Cervh7noGM9MQnbSD9lLHu9aQRp9Jl7vS4YPJI98IizYOzCndRcDKpv9LsJNJgXX3OVpxaUqbEiRVGcYMu7m72mdj4kNAyO86JejnaEgkFItUSg8jAU6DnkBwDHU

NOTE: The spec uses URL friendly base 64, which means + becomes -, / becomes _ and the = symbol is stripped from the end.

In part 2, we'll describe how to validate the token on the other end.

Post a Comment