philiprehberger-jwt_kit

Tests Gem Version Last updated

philiprehberger-jwt_kit

Opinionated JWT toolkit for Ruby — secure by default, with support for encoding, validation, refresh tokens, revocation, and key rotation

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-jwt_kit"

Or install directly:

gem install philiprehberger-jwt_kit

Usage

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

Configuration

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

Encoding

token = Philiprehberger::JwtKit.encode(user_id: 42, role: "admin")
# => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHA..."

Claims exp, iat, iss, and jti are added automatically.

Decoding

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.

Token Pairs

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.

Refresh Tokens

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.

Revocation

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.

Token Introspection

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"=>...}

Expiration Check

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

Time to Expiry

Get the seconds remaining until the token's exp claim. Negative when expired, nil when the token is malformed or has no numeric exp. Useful for scheduling pre-emptive refreshes rather than reacting after the fact:

Philiprehberger::JwtKit.time_to_expiry(token)  # => 3599
# refresh when fewer than 60 seconds remain
Philiprehberger::JwtKit.refresh(refresh_token) if Philiprehberger::JwtKit.time_to_expiry(token).to_i < 60

Audience Validation

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

Token Validation

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" }

Key Rotation

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)

Revocation Cleanup

Remove old revocation entries to keep memory usage bounded:

# Remove entries older than 1 hour
Philiprehberger::JwtKit.revocation_store.cleanup!(max_age: 3600)

Custom Revocation Store

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

Lifecycle Callbacks

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.

API

Method Description
`JwtKit.configure { \ c\
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.time_to_expiry(token) Seconds remaining until exp; negative when expired, nil when unknown
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\
`Configuration#on_decode { \ payload\
`Configuration#on_refresh { \ new_token\
`Configuration#on_revoke { \ jti\

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT