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");