Implement Your Own JWT Handler: Understanding and Securing JSON Web Tokens
JSON Web Tokens (JWTs) are a cornerstone of modern web authentication, offering a compact and secure way to transmit information between parties. While many libraries exist to handle JWTs for you, understanding the underlying cryptography and common vulnerabilities is crucial for building truly secure applications. This “do-it-yourself” guide will walk you through implementing your own JWT handler in C#, demystifying the process, and highlighting potential pitfalls.
What is a JWT?
At its core, a JWT is a string that represents a set of claims. These claims are statements about an entity (typically a user) and additional data. A JWT typically consists of three parts, separated by dots:
Header: Contains metadata about the token, such as the token type (JWT) and the signing algorithm used (e.g., HMAC-SHA256 or RSA).
Payload: Contains the claims. These can be registered claims (standardized fields like
issfor issuer,expfor expiration time), public claims (custom claims defined by the JWT creator), or private claims (claims agreed upon by the sender and receiver).Signature: Used to verify that the sender of the JWT is who it says it is and to ensure that the message hasn’t been tampered with.
Here’s what a decoded JWT looks like:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
When encoded, these parts are Base64Url-encoded and concatenated with dots: Header.Payload.Signature.
Why Implement Your Own?
While libraries offer convenience, rolling your own JWT handler provides:
Deeper Understanding: You’ll gain intimate knowledge of how JWTs work, from encoding to signing and verification.
Enhanced Security Awareness: You’ll be forced to confront security considerations, like key management and algorithm selection, directly.
Customization: Tailor the implementation to specific needs, such as unique claim handling or integration with custom cryptographic modules.
The Building Blocks: Base64Url Encoding
Before diving into JWT construction, we need a robust Base64Url encoder/decoder. This is a slight variation of standard Base64, where + is replaced with -, / with _, and padding (=) is often omitted.
public static class Base64Url
{
public static string Encode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Replace('+', '-');
output = output.Replace('/', '_');
output = output.TrimEnd('=');
return output;
}
public static byte[] Decode(string input)
{
var output = input.Replace('-', '+');
output = output.Replace('_', '/');
switch (output.Length % 4)
{
case 0: break;
case 2: output += "=="; break;
case 3: output += "="; break;
default: throw new ArgumentException("Illegal base64url string!");
}
return Convert.FromBase64String(output);
}
}
Crafting the JWT: Header and Payload
Let’s define simple classes for our JWT header and payload.
public class JwtHeader
{
public string Alg { get; set; } // Algorithm
public string Typ { get; set; } // Type
}
public class JwtPayload
{
public string Sub { get; set; } // Subject
public string Name { get; set; }
public long Iat { get; set; } // Issued At
public long Exp { get; set; } // Expiration Time (optional, but highly recommended)
}
Now, let’s create a method to encode these into the first two parts of our JWT.
using System.Text.Json;
using System.Text;
public static class JwtEncoder
{
public static string EncodeJwt(JwtHeader header, JwtPayload payload, byte[] secretKey)
{
// 1. Encode Header
var headerJson = JsonSerializer.Serialize(header);
var encodedHeader = Base64Url.Encode(Encoding.UTF8.GetBytes(headerJson));
// 2. Encode Payload
var payloadJson = JsonSerializer.Serialize(payload);
var encodedPayload = Base64Url.Encode(Encoding.UTF8.GetBytes(payloadJson));
var dataToSign = $"{encodedHeader}.{encodedPayload}";
// 3. Generate Signature (we'll implement this next)
var signature = GenerateHmacSha256Signature(dataToSign, secretKey);
var encodedSignature = Base64Url.Encode(signature);
return $"{dataToSign}.{encodedSignature}";
}
// ... Signature generation methods will go here
}
The Crucial Part: Signing the Token
The signature is what makes a JWT secure. It’s calculated by taking the Base64Url-encoded header, the Base64Url-encoded payload, and a secret key, then applying a cryptographic algorithm. For this example, we’ll use HMAC SHA256.
using System.Security.Cryptography;
public static class JwtEncoder
{
// ... previous code ...
private static byte[] GenerateHmacSha256Signature(string data, byte[] secretKey)
{
using (var hmac = new HMACSHA256(secretKey))
{
return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
}
}
}
Verifying the JWT: The Other Side of the Coin
Receiving a JWT isn’t enough; you must verify its signature to ensure its authenticity and integrity.
public static class JwtDecoder
{
public static bool VerifyJwt(string jwt, byte[] secretKey, out JwtPayload? decodedPayload)
{
decodedPayload = null;
var parts = jwt.Split('.');
if (parts.Length != 3) return false;
var encodedHeader = parts[0];
var encodedPayload = parts[1];
var receivedSignature = parts[2];
var dataToVerify = $"{encodedHeader}.{encodedPayload}";
// Recalculate signature
var expectedSignatureBytes = JwtEncoder.GenerateHmacSha256Signature(dataToVerify, secretKey);
var expectedSignature = Base64Url.Encode(expectedSignatureBytes);
if (expectedSignature != receivedSignature)
{
return false; // Signature mismatch! Token is invalid or tampered with.
}
// Signature is valid, now decode payload
try
{
var payloadBytes = Base64Url.Decode(encodedPayload);
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
decodedPayload = JsonSerializer.Deserialize<JwtPayload>(payloadJson);
// Optional: Check expiration (exp claim)
if (decodedPayload?.Exp > 0 && DateTimeOffset.UtcNow.ToUnixTimeSeconds() > decodedPayload.Exp)
{
return false; // Token has expired
}
return true;
}
catch (JsonException)
{
return false; // Malformed payload
}
catch (FormatException)
{
return false; // Malformed base64url encoding
}
}
}
Common Pitfalls and Vulnerabilities
1. The “None” Algorithm Vulnerability ☠️
This is perhaps the most notorious JWT vulnerability. Some JWT implementations, when they see {"alg": "none"} in the header, might skip signature verification entirely. An attacker could craft a JWT with a none algorithm and arbitrary claims, and if your server doesn’t explicitly check and reject tokens with this algorithm, it will trust the malicious token.
Mitigation: Always explicitly define the allowed signing algorithms and reject any token that uses “none” or an unexpected algorithm.
In our C# example: Our VerifyJwt method implicitly relies on HMACSHA256. To explicitly protect against “none”, you’d add:
// Inside VerifyJwt, after splitting parts
var headerBytes = Base64Url.Decode(encodedHeader);
var headerJson = Encoding.UTF8.GetString(headerBytes);
var header = JsonSerializer.Deserialize<JwtHeader>(headerJson);
if (header?.Alg == "none" || header?.Alg != "HS256") // Assuming HS256 is our ONLY allowed algorithm
{
return false; // Reject tokens with 'none' or unexpected algorithms
}
2. Weak Secret Keys
The security of HMAC-based JWTs hinges entirely on the secrecy and strength of your secretKey. If an attacker obtains your secret key, they can forge valid JWTs at will.
Mitigation:
Use long, randomly generated, cryptographically secure keys (e.g., 256 bits or more).
Store keys securely (e.g., in environment variables, hardware security modules, or a secure key vault).
Never hardcode keys in your source code.
3. No Expiration (exp) Claim
JWTs are typically used for a limited time. If a JWT doesn’t expire, and it’s ever compromised, it remains valid indefinitely.
Mitigation: Always include an exp (expiration time) claim in your payload and enforce its verification. Our VerifyJwt method already includes this check.
4. Not Validating All Claims
While the signature verifies the token’s integrity, you still need to validate the claims within the payload according to your application’s logic (e.g., checking the issuer iss, audience aud, and subject sub).
Mitigation: After successful signature verification, implement business logic to validate critical claims.
Conclusion
Implementing your own JWT handler is a rewarding exercise that provides invaluable insight into the mechanics and security considerations of JSON Web Tokens. By understanding the encoding, signing, and verification processes, and by being acutely aware of common vulnerabilities like the “None” algorithm, you’ll be far better equipped to design and implement secure authentication systems in your C# applications. While production applications will often benefit from battle-tested libraries, this DIY approach solidifies the foundational knowledge essential for any developer working with JWTs.



