Skip to content
Ori Pekelman edited this page May 11, 2026 · 1 revision

Tep::Jwt

HS256 JWT encode / verify / decode. Interop-tested against the canonical jwt gem — produced tokens validate there, and tokens that gem produces validate here.

Scope

  • Algorithm: HS256 only. No RS256, ES256, none.
  • Header: {"alg":"HS256","typ":"JWT"} is hard-coded; users who need custom headers should look elsewhere.
  • Payload: must be a JSON string the user composes — see Tep::Json. The library doesn't enforce the standard claim set (iss/aud/exp/...) and doesn't auto-check expiry; both are the application's responsibility.

Encode

secret  = ENV.fetch("JWT_SECRET")
payload = Tep::Json.from_str_hash({"sub" => "alice", "exp" => "1700000000"})
token   = Tep::Jwt.encode_hs256(payload, secret)

Returns "<header>.<payload>.<sig>" base64url-encoded.

Verify + decode

Two flavours: separate steps (verify, then decode) or one-shot.

ok = Tep::Jwt.verify_hs256(token, secret)
if ok
  payload = Tep::Jwt.decode_payload(token)
  sub     = Tep::Json.get_str(payload, "sub")
end
payload = Tep::Jwt.verify_and_decode(token, secret)
if payload.length > 0
  sub = Tep::Json.get_str(payload, "sub")
end

verify_and_decode returns "" on signature mismatch — easier to chain than the bool-then-decode form.

Timing-safe equality

The signature comparison is constant-time. If you need the underlying primitive (e.g. for comparing API keys against a stored hash), the same helper is exposed:

Tep::Jwt.timing_safe_eq(actual, expected)   # true / false

Cookbook

Issue a token on login

post '/login' do
  if user_password_ok(params[:user], params[:password])
    h = Tep.str_hash
    h["sub"] = params[:user]
    h["exp"] = (Time.now.to_i + 3600).to_s
    payload = Tep::Json.from_str_hash(h)
    token = Tep::Jwt.encode_hs256(payload, JWT_SECRET)
    content_type 'application/json'
    Tep::Json.from_str_hash({"token" => token})
  else
    halt 401, "nope"
  end
end

Gate a route on a bearer token

before do
  auth = request.headers["authorization"]
  if !auth.start_with?("Bearer ")
    halt 401, "missing bearer"
  end
  token   = auth[7, auth.length - 7]
  payload = Tep::Jwt.verify_and_decode(token, JWT_SECRET)
  if payload.length == 0
    halt 401, "invalid token"
  end
  exp = Tep::Json.get_int(payload, "exp")
  if exp != 0 && exp < Time.now.to_i
    halt 401, "expired"
  end
  request.ivars["sub"] = Tep::Json.get_str(payload, "sub")
end

Pitfalls

  • Don't reuse the session secret. Tep.session_secret signs cookies; JWT_SECRET signs tokens. If you reuse the same secret, an attacker who steals a JWT can forge a session cookie (or vice versa).
  • No automatic expiry. The encoder writes exp if you put it in the payload, but verify_hs256 won't reject expired tokens — that's the app's exp < Time.now.to_i check.
  • Header is fixed. A token with a non-HS256 algorithm fails to verify, regardless of the header's alg claim. (This is the correct behaviour against the alg=none substitution attack; document the constraint when interoperating with other libraries.)

Clone this wiki locally