Generate JSON Web Token (JWT)

I needed a quick and easy way to generate a JWT which included some claims.

  1. Install these libraries
  1. Add some configuration in appsettings, the Secret can be anything as long as its a key size of at least 128 bits. I used this online GUID generator for testing.
1
2
3
4
5
6
"IdentityToken": {
"Secret": "78923ed7-259f-42fa-bfa2-505cab403d12",
"Issuer": "https://localhost:5008/",
"Audience": "https://localhost:5006/",
"ExpireDays": 7
}
  1. Build the service injecting the configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using AuthService.Application.Common;
using AuthService.Domain.Interfaces.Application;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace AuthService.Application.Services
{
public class IdentityTokenService : IIdentityTokenService
{
private readonly IdentityTokenOptions _options;

public IdentityTokenService(IOptions<IdentityTokenOptions> options)
{
_options = options.Value;
}

public string Get(Guid userId, string screens)
{
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_options.Key));
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim("screens", screens),
}),
Expires = DateTime.UtcNow.AddDays(_options.ExpireDays),
Issuer = _options.Issuer,
Audience = _options.Audience,
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
}
  1. A use case could be the exchange of an Authorization Code. The example below is from my SPA, here I am sending the SPA’s clientid and clientsecret along with the code the Authorisation Service returned at its login screen (that was the only time a username/password is required)

This is part of the Proof Key for Code Exchange (PKCE) flow (pronounced “pixy”).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { getAuthUrl, getClientId, getClientSecret } from '../common/EnvTools';

export const getToken = async (code) => {
const url = getAuthUrl()
const clientid = getClientId()
const clientsecret = getClientSecret()
const credentials = { clientid, clientsecret, code }

const data = new FormData();
data.append('credential', JSON.stringify(credentials));

const response = await fetch(`${url}/token`, {
method: 'POST',
body: data,
});

return await response.json();
};

The Authorisation Service could have a TokenController with implementation as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[HttpPost("/token")]
public async Task<ActionResult> Token([FromForm] string credential)
{
var source = new CancellationTokenSource();
var token = source.Token;
var clientOptions = _registeredClientOptions.PorkySpa;

var deserializedCredential = JsonSerializer.Deserialize<AuthorizationCodeModels>(credential, SerializerOptions.Deserialize);

if (deserializedCredential?.ClientSecret != clientOptions.Secret)
{
_logger.LogInformation($"ClientSecret unknown");
return Unauthorized();
}

if (deserializedCredential?.ClientId != clientOptions.ClientId)
{
_logger.LogInformation($"ClientId unknown.");
return Unauthorized();
}

var authCode = await _authCodeRepository.LoadAsync(deserializedCredential.Code, token);
_ = _authCodeRepository.DeleteAsync(deserializedCredential.Code, token);

var dbCredential = await _credentialsRepository.LoadAsync(authCode.Username, token);

if (dbCredential == null)
{
_logger.LogInformation("dbCredential is null, looked up on " + authCode.Username ?? "");
return Unauthorized();
}

var jwt = _tokenService.Get(dbCredential.UserId, dbCredential.Screens);

var response = new PokzerModels
{
Token = jwt,
Username = authCode.Username
};

return Ok(response);
}
  1. The resulting response for this call could be
1
2
3
4
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJkMTEwNGQ1MC0yN2NlLTQwZDEtYjc5OS1mZTA4MDczOTU4NzUiLCJzY3JlZW5zIjoidXBsb2FkZmlsZSxmaW5kLGYxbmR0bCxyZXBvcnQsZWFzdGVyIiwibmJmIjoxNjU2MTk5MTIwLCJleHAiOjE2NTY4MDM5MjAsImlhdCI6MTY1NjE5OTEyMCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwOC8iLCJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo1MDA2LyJ9.jtT-8Eq3YsqXhr-zCFuk84Jsyo4ldgepZRR1XiSmn1Y",
"displayName": "Porky"
}
  1. Decoded using jwt.io the payload could look as follows, the specification on these properties is rfc7519.

Production JWTs should never be popped into any site like jwt.io, this is a security threat and you will get fired (again) -_-

Header

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

Payload

1
2
3
4
5
6
7
8
9
{
"nameid": "d1104d50-27ce-40d1-b799-fe0807395875",
"screens": "uploadfile,find,f1ndtl,report,easter",
"nbf": 1656199120,
"exp": 1656803920,
"iat": 1656199120,
"iss": "https://localhost:5008/",
"aud": "https://localhost:5006/"
}

Details about the payload can be found at https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 and OAuth2 - Delegation Token

References