Recap: What makes up a JWT
Before we dive into the code, let’s remind ourselves what a JWT really is. It’s three Base64URL-encoded pieces of data joined by three dots. For example:
header.payload.signature
Header
This describes the algorithm and token type as follows:
{
"alg": "HS256",
"typ": "JWT"
}
In this example the algorithm is HMAC using SHA-256 but JWTs can also be unsecured (meaning the algo would be "none") or it could be encrypted where the algo could "RSA1_5" and it’s enc value could be something like "A128CBC-HS256A128CBC-HS256".
Read the RFC information on unsecured tokens and encrypted tokens.
Payload
The payload holds the actual claims, which is another way of saying “statements about the user or the session.” These are just key-value pairs, something like:
{
"sub": 1,
"name": "Carlos",
"iat": 1700000000,
"exp": 1700000900
}
Some of these keys are standardized such as:
sub- the subject (often the user id)iat- issued at (timestamp)exp- expiration timeiss- who issued the tokenaud- intended audience
You can also roll your own, like roles or scopes. Something similar to the following:
{
"role": "admin",
"scp": ["read:users"]
}
For a full break down of claims (including registered claims, public claims, and private claims) see RFC 7519.
Signature
For this tutorial we’ll be covering HMAC-SHA256 and put together it looks something like this:
base64url(header) + "." + base64url(payload) + "." + base64url(signature)
That’s it. Pretty much every library out there does something like the above for their HMAC-SHA256 implementation.
Manually creating a JWT?
Ruby
Plain Ruby
require 'json'
require "base64"
require "openssl"
payload = {
"sub" => 1,
"name" => "Carlos",
"iat" => 1_700_000_000,
"exp" => 1_700_000_900
}
header = { "alg": "HS256", "typ": "JWT" }
secret = "my$ecretKey"
# Helper to Base64URL-encode without padding
def base64url_encode(data)
Base64.urlsafe_encode64(data).delete("=")
end
header_encoded = base64url_encode(header.to_json)
payload_encoded = base64url_encode(payload.to_json)
data = "#{header_encoded}.#{payload_encoded}"
signature = OpenSSL::HMAC.digest("sha256", secret, data)
signature_encoded = base64url_encode(signature)
token = "#{data}.#{signature_encoded}"
If you inspect the contents of token it should look something like the following:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsIm5hbWUiOiJDYXJsb3MiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMDkwMH0.E6NvfkOhIlIKwOb2KgnY57267wGwlMAhkS9DpER78l4"
Ruby using jwt gem
require 'jwt'
payload = { data: 'test', exp: Time.now.to_i + 3600 }
hmac_secret = 'my$ecretK3y'
token = JWT.encode(payload, hmac_secret, 'HS256')
# To decode:
JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
Python
import json, base64, hmac, hashlib
header = {"alg": "HS256", "typ": "JWT"}
payload = {
"sub": 1,
"name": "Carlos",
"iat": 1700000000,
"exp": 1700000900
}
secret = b"my$ecretKey"
# Helper to Base64URL-encode without padding
def base64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("utf-8")
header_encoded = base64url_encode(json.dumps(header, separators=(",", ":")).encode())
payload_encoded = base64url_encode(json.dumps(payload, separators=(",", ":")).encode())
data = f"{header_encoded}.{payload_encoded}".encode("utf-8")
signature = hmac.new(secret, data, hashlib.sha256).digest()
signature_encoded = base64url_encode(signature)
token = f"{header_encoded}.{payload_encoded}.{signature_encoded}"
JavaScript
// Some environments may not have Buffer as a global
import { Buffer } from "buffer";
// For Node.js environments (browsers don't need this)
import crypto from "crypto";
const header = { alg: "HS256", typ: "JWT" };
const payload = {
sub: 1,
name: "Carlos",
iat: 1700000000,
exp: 1700000900
};
const secret = "my$ecretKey";
// Helper to Base64URL-encode without padding
const base64urlEncode = (input) =>
Buffer.from(input)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
const headerEncoded = base64urlEncode(JSON.stringify(header));
const payloadEncoded = base64urlEncode(JSON.stringify(payload));
const data = `${headerEncoded}.${payloadEncoded}`;
const signature = crypto.createHmac("sha256", secret).update(data).digest("base64");
const signatureEncoded = signature
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
const token = `${headerEncoded}.${payloadEncoded}.${signatureEncoded}`;
Go
package main
import (
cryptoRand "crypto/rand"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
)
func base64url(data []byte) string {
return base64.RawURLEncoding.EncodeToString(data) // URL-safe, no padding
}
func main() {
// Header and payload
header := map[string]string{
"alg": "HS256",
"typ": "JWT",
}
payload := map[string]interface{}{
"sub": 1,
"name": "Carlos",
"iat": 1700000000,
"exp": 1700000900,
}
// JSON encode header and payload
headerJSON, _ := json.Marshal(header)
payloadJSON, _ := json.Marshal(payload)
// Base64URL encode
headerB64 := base64url(headerJSON)
payloadB64 := base64url(payloadJSON)
// Data to sign
data := headerB64 + "." + payloadB64
// Secret key (bytes). Replace with your real secret.
secret := []byte("my$ecretKey")
// HMAC-SHA256 signature
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(data))
sig := mac.Sum(nil)
// Base64URL encode signature
sigB64 := base64url(sig)
// Final token
token := data + "." + sigB64
fmt.Println(token)
// Optional: show a random kid example if you want to teach header fields
_ = cryptoRand.Reader // referenced so import stays if you add kid generation later
}
Elixir
mix.exs
def deps do
[
{:jason, "~> 1.4"}
]
end
jwt.exs
# mix deps.get first if you're in a Mix project
header = %{"alg" => "HS256", "typ" => "JWT"}
payload = %{
"sub" => 1,
"name" => "Carlos",
"iat" => 1_700_000_000,
"exp" => 1_700_000_900
}
secret = "my$ecretKey"
# Base64URL without padding
b64url = fn bin -> Base.url_encode64(bin, padding: false) end
# Encode header and payload to compact JSON, then Base64URL
header_b64 = header |> Jason.encode!() |> b64url.()
payload_b64 = payload |> Jason.encode!() |> b64url.()
data = header_b64 <> "." <> payload_b64
# HMAC-SHA256 signature, then Base64URL without padding
sig =
:crypto.mac(:hmac, :sha256, secret, data)
|> b64url.()
token = data <> "." <> sig
Coming Up
Now that we’ve built JWTs from scratch in a few different languages, it’s time to see them in action. In the next article, we’ll plug everything into a Rails API and explore the practical side such as issuing tokens, validating them, and keeping them fresh.