C# - How To Create a JWT

Nuget Requirements

  1. Microsoft.IdentityModel.Tokens
  2. System.IdentityModel.Tokens.Jwt
  3. Newtonsoft

Intro

A straight forward way to create JWTs thats geared towards Asp.Net Identity. Converting to Token based identity management for login can be intimidating but don't let it stop you! Here is a quick (read as not perfect) way to get your hands on creating valid JWTs. I am going to take some liberties and assume a single service is responsible for Authorizing and Issuing tokens. Once you feel confident with this material then you can use an authority/Identity server with this code too.

RED ALERT

Teeny tiny security note, these tokens are not Encrypted. So don't put PCI / PII etc. data inside the token if you don't want people reading it. These tokens DO however have Sha256 hashed signatures.

Models

Up first is creating some Identity models. These should correlate to user data taken from the database/redis/bag of holding etc. For this example we have greatly simplified this part.

// Sample User Model
public class User
{
    public int UserId { get; set; }
    public string EmailAddress { get; set; }
    public string FullName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// Sample User Meta Data
public class UserData
{
    public int AccountId { get; set; }
}

// Sample User Role
public class Role
{
    public int UserId { get; set; }
    public int RoleId { get; set; }
    public string RoleName { get; set; }
}

Creating The Claims

Now we are going to get the user roles from the database. I have left that part out of this example. Using EFCore or Dapper or plain Ado.Net to query your database is up to you. I am generally against putting comments in the examples but I think its important to label some of what we are actually doing.

public static Task<ClaimsIdentity> CreateClaimsIdentitiesAsync(User user, UserData userData)
{
    ClaimsIdentity claimsIdentity = new ClaimsIdentity();
    claimsIdentity.AddClaim(new Claim(ClaimTypes.Email, user.EmailAddress));
    claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
    claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, user.FullName ?? $"{user.FirstName} {user.LastName}"));

    // This next one is kinda of special. This lets you put non-convential JWT data in here in the format you desire.
    // DO NOT ABUSE IT! If your tokens get too fat you aren't using them as intended (as identity). You
    // will only hurt yourself in the long run.
    claimsIdentity.AddClaim(new Claim(ClaimTypes.UserData, JsonConvert.SerializeObject(userData)));
    // Keep the UserData field small, such as just Ids that can then be used to look up data you need.
    // Instead of getting just a UserId and having to look up their LibraryId, you can  go straight
    // to using a LibraryId stored in the user data to look up books checked out.
    // It accepts any string as value, so it could be json, or csv, or tsv, etc. I chose a Json string for this
    // demo.

    // TODO: Roles. You need to connect to your database here to get the ACTUAL roles that you may
    // already be using. Some common examples are Security.UserRoles or dbo.UserRoles. Wherever your
    // roles are, you need to do that. Your schema is up for you to figure out or even create.
    // ex.) var roles = await _userRepository.GetRolesAsync(userId);
    // Use the Role model as an example of the data you need to acquire.
    // Roles could be Student, Employee, Trainer, Admin, etc.
    var roles = Enumerable.Empty<Role>(); // Not a real list.

    foreach (var role in roles)
    { claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role.RoleName)); }

    return Task.FromResult(claimsIdentity);
}

Creating The Token

public static async Task<string> CreateJWTAsync(
    User user,
    UserData userData,
    string issuer,
    string authority,
    string symSec,
    int daysValid)
{
    var tokenHandler = new JwtSecurityTokenHandler();
    var claims = await CreateClaimsIdentitiesAsync(user, userData);

    // Create JWToken
    var token = tokenHandler.CreateJwtSecurityToken(issuer: issuer,
        audience: authority,
        subject: claims,
        notBefore: DateTime.UtcNow,
        expires: DateTime.UtcNow.AddDays(daysValid),
        signingCredentials:
        new SigningCredentials(
            new SymmetricSecurityKey(
                Encoding.Default.GetBytes(symSec)),
                SecurityAlgorithms.HmacSha256Signature));

    return tokenHandler.WriteToken(token);
}

Here you can see with some basic inputs, you can create real JWTs. We used Sha256 to digitally sign signature. Anyone can decode a token but no one can man in the middle replace data with a valid signed signature (yet anyways). New developers need to know that for security reasons (guard your private keys!).

Everything Together

using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace JwtExamples
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var user = new User { UserId = 1, EmailAddress = "cat@houseofcat.io", FirstName = "House", LastName = "Cat" };
            var userData = new UserData { AccountId = 1 };
            var issuer = "https://houseofcat.io";
            var authority = "https://houseofcat.io"; // or https://authorityprovider.com/ etc

            //Issuer & Authority Note: If your app or API is the issuer and authority they will be the exact same field. They but don't have to be, key distinction.

            //256-bit string generated on https://passwordsgenerator.net/
            var privateKey = "J6k2eVCTXDp5b97u6gNH5GaaqHDxCmzz2wv3PRPFRsuW2UavK8LGPRauC4VSeaetKTMtVmVzAC8fh8Psvp8PFybEvpYnULHfRpM8TA2an7GFehrLLvawVJdSRqh2unCnWehhh2SJMMg5bktRRapA8EGSgQUV8TCafqdSEHNWnGXTjjsMEjUpaxcADDNZLSYPMyPSfp6qe5LMcd5S9bXH97KeeMGyZTS2U8gp3LGk2kH4J4F3fsytfpe9H9qKwgjb";
            var daysValid = 7;

            var createJwt = await CreateJWTAsync(user, userData, issuer, authority, privateKey, daysValid);

            await Console.Out.WriteLineAsync(createJwt);
            await Console.In.ReadLineAsync();
        }

        public static async Task<string> CreateJWTAsync(
            User user,
            UserData userData,
            string issuer,
            string authority,
            string symSec,
            int daysValid)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var claims = await CreateClaimsIdentities(user, userData);

            // Create JWToken
            var token = tokenHandler.CreateJwtSecurityToken(issuer: issuer,
                audience: authority,
                subject: claims,
                notBefore: DateTime.UtcNow,
                expires: DateTime.UtcNow.AddDays(daysValid),
                signingCredentials:
                new SigningCredentials(
                    new SymmetricSecurityKey(
                        Encoding.Default.GetBytes(symSec)),
                        SecurityAlgorithms.HmacSha256Signature));

            return tokenHandler.WriteToken(token);
        }

        public static Task<ClaimsIdentity> CreateClaimsIdentities(User user, UserData userData)
        {
            ClaimsIdentity claimsIdentity = new ClaimsIdentity();
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Email, user.EmailAddress));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, user.FullName ?? $"{user.FirstName} {user.LastName}"));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.UserData, JsonConvert.SerializeObject(userData)));

            var roles = Enumerable.Empty<Role>(); // Not a real list.

            foreach (var role in roles)
            { claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role.RoleName)); }

            return Task.FromResult(claimsIdentity);
        }

        // Sample User Model
        public class User
        {
            public int UserId { get; set; }
            public string EmailAddress { get; set; }
            public string FullName { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }

        // Sample User Meta Data
        public class UserData
        {
            public int AccountId { get; set; }
        }

        // Sample User Role
        public class Role
        {
            public int UserId { get; set; }
            public int RoleId { get; set; }
            public string RoleName { get; set; }
        }
    }
}

Results

This generated a pretty beefy token.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNhdEBob3VzZW9mY2F0LmlvIiwibmFtZWlkIjoiMSIsInVuaXF1ZV9uYW1lIjoiSG91c2UgQ2F0IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy91c2VyZGF0YSI6IntcIkFjY291bnRJZFwiOjF9IiwibmJmIjoxNTUzOTU5MDg4LCJleHAiOjE1NTQ1NjM4ODgsImlhdCI6MTU1Mzk1OTA4OCwiaXNzIjoiaHR0cHM6Ly9ob3VzZW9mY2F0LmlvIiwiYXVkIjoiaHR0cHM6Ly9ob3VzZW9mY2F0LmlvIn0.9UdREJIBCbyeymL0qFmSPN0YdGe_NMlgiYL0MVtLFbw

Let's head over to Jwt.io and see if it is a legit token.

If we also grab our private key, we can optionally verify our signature.

J6k2eVCTXDp5b97u6gNH5GaaqHDxCmzz2wv3PRPFRsuW2UavK8LGPRauC4VSeaetKTMtVmVzAC8fh8Psvp8PFybEvpYnULHfRpM8TA2an7GFehrLLvawVJdSRqh2unCnWehhh2SJMMg5bktRRapA8EGSgQUV8TCafqdSEHNWnGXTjjsMEjUpaxcADDNZLSYPMyPSfp6qe5LMcd5S9bXH97KeeMGyZTS2U8gp3LGk2kH4J4F3fsytfpe9H9qKwgjb


Sources

Jwt.io
Auth0
JwtSecurityTokenHandler.CreateJwtSecurityToken Method