How to Forge a JWT with C#

When running tests on APIs we need to be able to create tokens that have specific characteristics to make sure the API responds correctly to each test scenario. I developed a C# script that allows me to do just that. very simple, it can be improved and more options can be added but for my use case it's enough. All-manual operation, no calls to JWT security token descriptors or anything.

You need to run this script with dotnet-script jwtForger.csx. Here is the code:

using System.Text.Json;
using System.Security.Cryptography;

// JSON configuration for the JWT.
// Using a raw string literal for easier JSON editing. No escaping required.
string configJson = """
{
  "Header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "Payload": {
    "aud": [
      "you",
      "andYou"
    ],
    "some": "claim",
    "nbf": "<now>",
    "exp": "<years:3>",
    "iat": "<now>",
    "sub": "me",
    "iss": "jwtForger.csx"
  },
  "Signature": {
    "SecretKey": "SuperSecretKey84389248#@%$$&"
  }
}
""";

// Function to perform Base64-URL encoding
string Base64UrlEncode(string base64)
{
    return base64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
}

// Deserialize the configuration into a dictionary.
var config = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(configJson)!;

// Extract header values.
string algorithm = config["Header"].GetProperty("alg").GetString()!;
string type = config["Header"].GetProperty("typ").GetString()!;

// Extract the payload section.
JsonElement payloadElement = config["Payload"];

// Extract signature information.
string secretKey = config["Signature"].GetProperty("SecretKey").GetString()!;

// Build a dictionary for claims from the payload.
// We convert special tokens (e.g. "<now>", "<midnight>", "<hours:2>", etc.) into Unix time (seconds) when needed.
var allClaims = new Dictionary<string, object>();
foreach (var prop in payloadElement.EnumerateObject())
{
    string claimName = prop.Name; 
    // Console.WriteLine($"Processing claim: {claimName}"); // DEBUG
    if (prop.Value.ValueKind == JsonValueKind.String)
    {
        string value = prop.Value.GetString()!;
        // Special processing for time-related tokens.
        if (value == "<now>")
        {
            long nowUnix = new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();
            allClaims[claimName] = nowUnix;
        }
        else if (value == "<midnight>")
        {
            var now = DateTime.Now;
            var midnight = new DateTime(now.Year, now.Month, now.Day, 23, 59, 59);
            long midnightUnix = new DateTimeOffset(midnight).ToUnixTimeSeconds();
            allClaims[claimName] = midnightUnix;
        }
        else if (value.StartsWith("<hours:") && value.EndsWith(">"))
        {
            int hours = int.Parse(value.Substring(7, value.Length - 8));
            long unixTime = new DateTimeOffset(DateTime.Now.AddHours(hours)).ToUnixTimeSeconds();
            allClaims[claimName] = unixTime;
        }
        else if (value.StartsWith("<days:") && value.EndsWith(">"))
        {
            int days = int.Parse(value.Substring(6, value.Length - 7));
            // For "<days:1>" we assume expiration is today at 23:59:59,
            // "<days:2>" means tomorrow 23:59:59, etc.
            var now = DateTime.Now;
            var expTime = new DateTime(now.Year, now.Month, now.Day, 23, 59, 59).AddDays(days - 1);
            long unixTime = new DateTimeOffset(expTime).ToUnixTimeSeconds();
            allClaims[claimName] = unixTime;
        }
        else if (value.StartsWith("<weeks:") && value.EndsWith(">"))
        {
            int weeks = int.Parse(value.Substring(7, value.Length - 8));
            var now = DateTime.Now;
            var expTime = new DateTime(now.Year, now.Month, now.Day, 23, 59, 59).AddDays(weeks * 7);
            long unixTime = new DateTimeOffset(expTime).ToUnixTimeSeconds();
            allClaims[claimName] = unixTime;
        }
        else if (value.StartsWith("<months:") && value.EndsWith(">"))
        {
            int months = int.Parse(value.Substring(8, value.Length - 9));
            var now = DateTime.Now;
            var expTime = new DateTime(now.Year, now.Month, now.Day, 23, 59, 59).AddMonths(months);
            long unixTime = new DateTimeOffset(expTime).ToUnixTimeSeconds();
            allClaims[claimName] = unixTime;
        }
        else if (value.StartsWith("<years:") && value.EndsWith(">"))
        {
            int years = int.Parse(value.Substring(7, value.Length - 8));
            var now = DateTime.Now;
            var expTime = new DateTime(now.Year, now.Month, now.Day, 23, 59, 59).AddYears(years);
            long unixTime = new DateTimeOffset(expTime).ToUnixTimeSeconds();
            allClaims[claimName] = unixTime;
        }
        else
        {
            // For normal string values, use them as-is.
            allClaims[claimName] = value;
        }
    }
    else if (prop.Value.ValueKind == JsonValueKind.Array)
    {
        // Deserialize arrays into List<string>.
        var list = JsonSerializer.Deserialize<List<string>>(prop.Value.GetRawText());
        allClaims[claimName] = list!;
    }
    else
    {
        // For other kinds, store the raw text.
        allClaims[claimName] = prop.Value.GetRawText();
    }
    // Console.WriteLine($"Claim {claimName} added with value: {allClaims[claimName]}"); // DEBUG
}
// Console.WriteLine($"\n> All claims extracted from the payload: {JsonSerializer.Serialize(allClaims, new JsonSerializerOptions {WriteIndented = true} )}\n"); // DEBUG

//----------------------------------
// ---------- FORGING! -------------
//----------------------------------

/* --- JWT HEADER --- */

// Extract the "Header" section from the configuration.
JsonElement headerElement = config["Header"];

// Serialize the header object to a JSON string.
string headerJson = JsonSerializer.Serialize(headerElement, new JsonSerializerOptions { WriteIndented = false });

// Convert the JSON string to Base64.
string headerBase64Url = Base64UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(headerJson)));

// Output the Base64-encoded header.
// Console.WriteLine($"\n> Base64-encoded Header:\n\n{headerBase64Url}\n"); // DEBUG


/* --- JWT PAYLOAD --- */

// Serialize the payload object to a JSON string.
string payloadJson = JsonSerializer.Serialize(allClaims, new JsonSerializerOptions {WriteIndented = false});

// Convert the JSON string to Base64.
string payloadBase6Url = Base64UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson)));

// Output the Base64-encoded payload.
// Console.WriteLine($"\n> Base64-encoded Payload:\n\n{payloadBase6Url}\n"); // DEBUG


/* --- JWT SIGNATURE --- */

// Convert the secret key to bytes.
byte[] keyBytes = Encoding.UTF8.GetBytes(secretKey);

// Combine the Base64-encoded header and payload to form the message.
string message = $"{headerBase64Url}.{payloadBase6Url}";

// Create the HMACSHA256 instance with the secret key.
string signatureBase64Url = string.Empty;

// Create the HMAC instance based on the "alg" value in the header.
using (HMAC hmac = algorithm switch
{
    "HS256" => new HMACSHA256(keyBytes),
    "HS384" => new HMACSHA384(keyBytes),
    "HS512" => new HMACSHA512(keyBytes),
    _ => throw new Exception($"Unsupported algorithm: {algorithm}")
})
{
    // Compute the HMAC for the message.
    byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));

    // Convert the hash to Base64-URL.
    signatureBase64Url = Base64UrlEncode(Convert.ToBase64String(hashBytes));

    // Output the signature.
    // Console.WriteLine($"\n> SHA256/384/512 HMAC Signature:\n\n{signatureBase64Url}\n"); // DEBUG
}


/* --- Final JWT Token --- */

// Combine the Base64-encoded header, payload, and signature to form the final JWT token.
Console.WriteLine($"\n> Final Forged JWT:\n\n{headerBase64Url}.{payloadBase6Url}.{signatureBase64Url}\n");