Introduction
JSON Web Tokens (JWT) are a stateless authentication mechanism where the server encodes user data into a signed token. The client stores this token and sends it with each request. Unlike database-backed tokens, JWTs don't require a database lookup on every request.
Key Concepts
- JWT (JSON Web Token): A self-contained token with three parts: header, payload, and signature.
- Stateless Authentication: The server doesn't store session data. All information is encoded in the token.
- Token Expiration: JWTs include an
expclaim that sets when the token becomes invalid. - Refresh Tokens: Long-lived tokens used to obtain new access tokens without re-authenticating.
Real World Context
JWTs are popular for APIs serving mobile apps and SPAs where session cookies aren't available. They scale well because any server can verify the token without hitting a database. However, they can't be revoked without additional infrastructure.
Deep Dive
Setting Up JWT
Add the jwt gem:
ruby# Gemfile gem "jwt"
Create a JWT service:
ruby# app/services/jwt_service.rb class JwtService SECRET = Rails.application.credentials.secret_key_base ALGORITHM = "HS256" def self.encode(payload, exp: 24.hours.from_now) payload[:exp] = exp.to_i JWT.encode(payload, SECRET, ALGORITHM) end def self.decode(token) decoded = JWT.decode(token, SECRET, true, algorithm: ALGORITHM) decoded.first.with_indifferent_access rescue JWT::DecodeError, JWT::ExpiredSignature nil end end
The service encodes user data with an expiration and decodes/verifies tokens. It uses the app's secret key base for signing.
Login Endpoint
rubydef create user = User.authenticate_by( email: params[:email], password: params[:password] ) if user token = JwtService.encode(user_id: user.id) render json: { token: token, expires_in: 24.hours.to_i } else render json: { error: "Invalid credentials" }, status: :unauthorized end end
The login endpoint returns a signed JWT containing the user ID.
Verifying Tokens
rubyclass ApplicationController < ActionController::API private def authenticate_user! token = request.headers["Authorization"]&.remove("Bearer ") payload = JwtService.decode(token) @current_user = User.find_by(id: payload&.dig(:user_id)) render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user end end
Each request extracts the token, decodes it, and finds the user. If the token is expired or invalid, decode returns nil.
Common Pitfalls
- No revocation strategy — JWTs are valid until they expire. If a token is compromised, you can't invalidate it without maintaining a blocklist (which defeats the stateless benefit).
- Storing JWTs in localStorage — Vulnerable to XSS attacks. Use httpOnly cookies for browser clients.
Best Practices
- Set short expiration times — 15-60 minutes for access tokens, use refresh tokens for longer sessions.
- Include only necessary claims — Don't put sensitive data in the JWT payload. It's encoded, not encrypted.
Summary
- JWTs are stateless tokens encoding user data with a signature.
- The
jwtgem handles encoding and decoding with HMAC signing. - Tokens include expiration claims for automatic invalidation.
- JWTs scale well but can't be individually revoked without a blocklist.
- Use short-lived access tokens with refresh tokens for security.
Code Examples
class JwtService
SECRET = Rails.application.credentials.secret_key_base
def self.encode(payload, exp: 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET, "HS256")
end
def self.decode(token)
JWT.decode(token, SECRET, true, algorithm: "HS256")
.first.with_indifferent_access
rescue JWT::DecodeError, JWT::ExpiredSignature
nil
end
end