Opinionated JWT toolkit for Ruby — secure by default, with support for
gem install philiprehberger-jwt_kitOpinionated JWT toolkit for Ruby — secure by default, with support for encoding, validation, refresh tokens, revocation, and key rotation
Add to your Gemfile:
gem "philiprehberger-jwt_kit"
Or install directly:
gem install philiprehberger-jwt_kit
require "philiprehberger/jwt_kit"
Philiprehberger::JwtKit.configure do |c|
c.secret = "your-secret-key-at-least-32-characters"
c.issuer = "my-app"
end
token = Philiprehberger::JwtKit.encode(user_id: 42)
payload = Philiprehberger::JwtKit.decode(token)
payload["user_id"] # => 42
Philiprehberger::JwtKit.configure do |c|
c.secret = "your-secret-key" # Required — HMAC signing key
c.algorithm = :hs256 # :hs256 (default), :hs384, :hs512
c.issuer = "my-app" # Optional — sets the `iss` claim
c.expiration = 3600 # Access token TTL in seconds (default: 1 hour)
c.refresh_expiration = 86_400 * 7 # Refresh token TTL (default: 1 week)
end
token = Philiprehberger::JwtKit.encode(user_id: 42, role: "admin")
# => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHA..."
Claims exp, iat, iss, and jti are added automatically.
payload = Philiprehberger::JwtKit.decode(token)
payload["user_id"] # => 42
payload["exp"] # => 1711036800
payload["iss"] # => "my-app"
payload["jti"] # => "a1b2c3d4-..."
Decoding validates the signature, expiration, and issuer automatically.
access_token, refresh_token = Philiprehberger::JwtKit.token_pair(user_id: 42)
The access token uses the standard expiration. The refresh token uses refresh_expiration and includes a type: "refresh" claim.
new_access_token = Philiprehberger::JwtKit.refresh(refresh_token)
Validates the refresh token, verifies it has type: "refresh", and issues a new access token with the original payload.
Philiprehberger::JwtKit.revoke(token)
Philiprehberger::JwtKit.revoked?(token) # => true
Philiprehberger::JwtKit.decode(token) # => raises RevokedToken
Revocation uses an in-memory store keyed by JTI. The store is thread-safe.
Decode a token without verifying its signature — useful for inspecting claims or determining which key to use:
result = Philiprehberger::JwtKit.peek(token)
result[:header] # => {"alg"=>"HS256", "typ"=>"JWT"}
result[:payload] # => {"user_id"=>42, "exp"=>..., "iat"=>..., "jti"=>...}
Check whether a token's exp claim is in the past without verifying the signature:
Philiprehberger::JwtKit.expired?(token) # => false
# Use to decide whether to refresh before the authoritative decode
Philiprehberger::JwtKit.configure do |c|
c.secret = "secret"
c.audience = "my-api" # string or array of strings
end
# Tokens automatically include the `aud` claim
token = Philiprehberger::JwtKit.encode(user_id: 42)
# Decoding validates the audience matches configuration
Philiprehberger::JwtKit.decode(token) # => raises InvalidAudience if mismatch
Returns a result hash instead of raising exceptions:
result = Philiprehberger::JwtKit.validate(token)
# => { valid: true, payload: { "user_id" => 42, ... }, error: nil }
result = Philiprehberger::JwtKit.validate(expired_token)
# => { valid: false, payload: nil, error: "Token has expired" }
Configure multiple secrets with key IDs for seamless key rotation:
Philiprehberger::JwtKit.configure do |c|
c.secrets = [
{ kid: "key-2024", secret: "new-secret-key" }, # Used for signing
{ kid: "key-2023", secret: "old-secret-key" } # Still accepted for verification
]
end
# Encodes using the first secret, adds `kid` to the JWT header
token = Philiprehberger::JwtKit.encode(user_id: 42)
# Decoding reads `kid` from the header and finds the matching secret
payload = Philiprehberger::JwtKit.decode(token)
Remove old revocation entries to keep memory usage bounded:
# Remove entries older than 1 hour
Philiprehberger::JwtKit.revocation_store.cleanup!(max_age: 3600)
Replace the default in-memory store with any object that responds to #revoke, #revoked?, #clear, and #size:
# Example: plug in a Redis-backed store
Philiprehberger::JwtKit.revocation_store = MyRedisRevocationStore.new
Hook into encode, decode, refresh, and revoke without monkey-patching. Useful for audit logging, metrics, and tracing:
Philiprehberger::JwtKit.configure do |c|
c.secret = 'your-secret-key'
c.on_encode do |token, payload|
Metrics.increment('jwt.encoded', tags: { iss: payload['iss'] })
end
c.on_decode do |payload|
Audit.log('jwt.decoded', user_id: payload['user_id'], jti: payload['jti'])
end
c.on_refresh do |new_token|
Metrics.increment('jwt.refreshed')
end
c.on_revoke do |jti|
Audit.log('jwt.revoked', jti: jti)
end
end
Callbacks fire only after a successful operation. Exceptions raised inside a callback are swallowed so they cannot break the calling JWT operation.
| Method | Description |
|---|---|
JwtKit.configure { |c| ... } | Configure secret, algorithm, issuer, and expiration |
JwtKit.configuration | Returns the current configuration |
JwtKit.reset_configuration! | Resets configuration to defaults |
JwtKit.encode(payload) | Encodes a payload into a signed JWT token |
JwtKit.decode(token) | Decodes and validates a JWT token |
JwtKit.validate(token) | Validates a token, returns result hash instead of raising |
JwtKit.token_pair(payload) | Generates an access/refresh token pair |
JwtKit.refresh(refresh_token) | Issues a new access token from a refresh token |
JwtKit.revoke(token) | Revokes a token by its JTI |
JwtKit.revoked?(token) | Checks if a token has been revoked |
JwtKit.peek(token) | Decode header and payload without signature verification |
JwtKit.expired?(token) | Check exp claim without verifying the signature |
JwtKit.revocation_store= | Set a custom revocation store |
MemoryStore#cleanup!(max_age:) | Remove revocation entries older than max_age seconds |
Configuration#on_encode { |token, payload| ... } | Register a callback fired after a successful encode |
Configuration#on_decode { |payload| ... } | Register a callback fired after a successful decode |
Configuration#on_refresh { |new_token| ... } | Register a callback fired after a successful refresh |
Configuration#on_revoke { |jti| ... } | Register a callback fired after a successful revoke |
bundle install
bundle exec rspec
bundle exec rubocop
If you find this project useful: