The project is in a healthy, maintained state
A complete JWT toolkit for Ruby. Encode and decode tokens with automatic claim management (exp, iat, iss, jti), generate access/refresh token pairs, validate expiration and issuer, and revoke tokens — all without external dependencies.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-jwt_kit

Tests Gem Version Last updated

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

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

API

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.revocation_store= Set a custom revocation store
MemoryStore#cleanup!(max_age:) Remove revocation entries older than max_age seconds

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