Skip to content

Using JSON Web Tokens in a Rails API - Part Two

Published: at 12:39 AM

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

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:

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.


Next Post
Using JSON Web Tokens in a Rails API - Part One