Hanko Ruby SDK
Ruby SDK for the Hanko authentication platform. Verify sessions, manage users via the Admin API, drive login/registration flows, and handle webhooks -- all from Ruby.
Gems
| Gem | Description |
|---|---|
| hanko-ruby | Core Ruby client (framework-agnostic). Session verification, Admin API, Flow API, webhook verification. |
| hanko-rails | Rails integration. Rack middleware, controller concern, install generator, and test helpers. |
Installation
Core gem only (any Ruby app)
# Gemfile
gem 'hanko-ruby'With Rails integration
# Gemfile
gem 'hanko-rails' # pulls in hanko-ruby automaticallyThen run:
bundle installQuick Start
Verify a Hanko session token in three lines:
require 'hanko'
Hanko.configure do |config|
config.api_url = 'https://your-instance.hanko.io'
end
client = Hanko::Client.new
result = client.public.sessions.validate_token(session_token)
puts result['user_id']Configuration
Global configuration
Set defaults that apply to every Hanko::Client instance:
Hanko.configure do |config|
config.api_url = ENV.fetch('HANKO_API_URL')
config.api_key = ENV.fetch('HANKO_API_KEY') # required for Admin API
config.timeout = 5 # request timeout in seconds (default: 5)
config.open_timeout = 2 # connection open timeout (default: 2)
config.retry_count = 1 # number of retries (default: 1)
config.clock_skew = 0 # allowed JWT clock skew in seconds (default: 0)
config.jwks_cache_ttl = 3600 # JWKS cache lifetime in seconds (default: 3600)
config.logger = Rails.logger
config.log_level = :info # default: :info
endPer-client overrides
Override any setting on a specific client instance:
admin_client = Hanko::Client.new(
api_url: 'https://your-instance.hanko.io',
api_key: ENV.fetch('HANKO_API_KEY'),
timeout: 10
)Global and per-client settings can be mixed; per-client values take precedence.
Session Verification
The primary use case: verify a Hanko JWT using the instance's JWKS endpoint.
Via the Public API
client = Hanko::Client.new
# Validate using a session token string
result = client.public.sessions.validate_token(session_token)
# Validate using cookie/header (server-side forwarding)
result = client.public.sessions.validateVia WebhookVerifier (standalone)
For lightweight verification without a full client:
payload = Hanko::WebhookVerifier.verify(
token,
jwks_url: 'https://your-instance.hanko.io/.well-known/jwks.json'
)
puts payload['sub'] # user IDThe verifier pins to RS256 and raises Hanko::InvalidTokenError or Hanko::ExpiredTokenError on failure.
Admin API
The Admin API requires an API key. All responses are wrapped in Hanko::Resource objects that support both hash-style (resource['field']) and method-style (resource.field) access.
Users
client = Hanko::Client.new
# List users
users = client.admin.users.list
users.each { |u| puts u.id }
# Get a specific user
user = client.admin.users.get('user-uuid')
# Create a user
user = client.admin.users.create(email: 'alice@example.com')
# Delete a user
client.admin.users.delete('user-uuid')User-scoped resources
Access emails, passwords, sessions, WebAuthn credentials, and metadata through a user context:
user_ctx = client.admin.users('user-uuid')
# Emails
emails = user_ctx.emails.list
user_ctx.emails.create(address: 'new@example.com')
user_ctx.emails.make_primary('email-uuid')
user_ctx.emails.delete('email-uuid')
# Passwords
user_ctx.passwords.create(password: 'new-password')
user_ctx.passwords.get
user_ctx.passwords.update(password: 'updated-password')
user_ctx.passwords.delete
# Sessions
sessions = user_ctx.sessions.list
user_ctx.sessions.delete('session-uuid')
# WebAuthn credentials
creds = user_ctx.webauthn_credentials.list
user_ctx.webauthn_credentials.delete('credential-uuid')
# Metadata
meta = user_ctx.metadata.get
user_ctx.metadata.update(custom_key: 'custom_value')Webhooks
# List webhooks
webhooks = client.admin.webhooks.list
# Create a webhook
webhook = client.admin.webhooks.create(
callback: 'https://example.com/webhooks/hanko',
events: ['user.create', 'user.delete']
)
# Delete a webhook
client.admin.webhooks.delete('webhook-uuid')Audit Logs
logs = client.admin.audit_logs.list
logs.each { |log| puts "#{log.type} at #{log.created_at}" }Flow API
Drive Hanko login and registration flows server-side. Returns Hanko::FlowResponse objects.
client = Hanko::Client.new
# Start a login flow
response = client.public.flow.login
puts response.status # :completed, :error, etc.
puts response.actions # available next actions
# Start a registration flow
response = client.public.flow.registration
# Profile flow
response = client.public.flow.profile
# Check flow state
if response.completed?
puts "User ID: #{response.user_id}"
puts "Session token: #{response.session_token}"
elsif response.error?
puts "Flow failed"
endWell-Known Endpoints
client = Hanko::Client.new
# Fetch JWKS
jwks = client.public.well_known.jwks
# Fetch Hanko configuration
config = client.public.well_known.configWebhook Verification
Verify incoming webhook payloads from Hanko:
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
token = request.headers['X-Hanko-Webhook-Token']
payload = Hanko::WebhookVerifier.verify(
token,
jwks_url: "#{ENV.fetch('HANKO_API_URL')}/.well-known/jwks.json"
)
case payload['evt']
when 'user.create'
User.create!(hanko_id: payload['sub'])
when 'user.delete'
User.find_by(hanko_id: payload['sub'])&.destroy
end
head :ok
rescue Hanko::InvalidTokenError, Hanko::ExpiredTokenError => e
head :unauthorized
end
endRails Integration
The hanko-rails gem provides automatic session verification, controller helpers, and an install generator.
Setup
# Generate the initializer
bin/rails generate hanko:installThis creates config/initializers/hanko.rb:
Hanko.configure do |config|
config.api_url = ENV.fetch('HANKO_API_URL', 'https://your-hanko-instance.hanko.io')
# config.api_key = ENV.fetch('HANKO_API_KEY', nil)
end
Hanko::Rails.configure do |config|
# config.cookie_name = 'hanko'
# config.exclude_paths = ['/healthz', '/up']
# config.jwks_cache_ttl = 3600
endMiddleware
The Hanko::Rails::Middleware is automatically inserted into the Rack stack by the engine. It:
- Extracts tokens from the
hankocookie orAuthorization: Bearerheader - Verifies the JWT against the JWKS endpoint
- Sets
request.env['hanko.session']with the decoded payload
Configure which paths to skip:
Hanko::Rails.configure do |config|
config.exclude_paths = ['/healthz', '/up', '/assets']
endAuthentication Concern
Include Hanko::Rails::Authentication in your controllers:
class ApplicationController < ActionController::Base
include Hanko::Rails::Authentication
before_action :authenticate_hanko_user!
endAvailable helpers:
| Helper | Returns |
|---|---|
hanko_session |
Decoded JWT payload hash, or nil
|
hanko_user_id |
The sub claim (user UUID), or nil
|
hanko_authenticated? |
true if a valid session exists |
current_hanko_user |
Alias for hanko_user_id
|
authenticate_hanko_user! |
Redirects (HTML) or returns 401 (JSON) if unauthenticated |
All helpers except authenticate_hanko_user! are also available in views.
Using with your User model
class ApplicationController < ActionController::Base
include Hanko::Rails::Authentication
def current_user
@current_user ||= User.find_by(hanko_id: hanko_user_id) if hanko_authenticated?
end
helper_method :current_user
endTesting
Both gems ship with test helpers to make it easy to write specs without hitting the Hanko API.
Core gem: Hanko::TestHelper
require 'hanko/test_helper'
# Generate a signed JWT for testing
token = Hanko::TestHelper.generate_test_token(
sub: 'user-uuid',
exp: Time.now.to_i + 3600
)
# Get a JWKS response body (matches the test signing key)
jwks_json = Hanko::TestHelper.test_jwks_response
# Stub the JWKS endpoint (requires WebMock)
Hanko::TestHelper.stub_jwks(api_url: 'https://your-instance.hanko.io')
# Create a stub verifier (no HTTP calls)
verifier = Hanko::TestHelper.stub_session(
sub: 'user-uuid',
exp: Time.now.to_i + 3600
)
payload = verifier.verify('any-token')
puts payload['sub'] # => 'user-uuid'Rails gem: Hanko::Rails::TestHelper
# spec/support/hanko.rb
RSpec.configure do |config|
config.include Hanko::Rails::TestHelper, type: :request
end
# In your request specs
RSpec.describe 'Dashboard', type: :request do
it 'requires authentication' do
get '/dashboard'
expect(response).to have_http_status(:unauthorized)
end
it 'shows dashboard for authenticated user' do
sign_in_as_hanko_user('user-uuid')
get '/dashboard'
expect(response).to have_http_status(:ok)
end
endError Handling
All errors inherit from Hanko::Error. The hierarchy:
Hanko::Error
├── Hanko::ConfigurationError # missing or invalid configuration
├── Hanko::InvalidTokenError # JWT signature invalid or malformed
├── Hanko::ExpiredTokenError # JWT has expired
├── Hanko::JwksError # failed to fetch or parse JWKS
├── Hanko::ConnectionError # network-level failure
└── Hanko::ApiError # HTTP 4xx/5xx response
├── Hanko::AuthenticationError # 401 Unauthorized
├── Hanko::NotFoundError # 404 Not Found
└── Hanko::RateLimitError # 429 Too Many Requests
Catching errors
begin
client.admin.users.get('nonexistent-uuid')
rescue Hanko::NotFoundError => e
puts "User not found (HTTP #{e.status})"
rescue Hanko::RateLimitError => e
puts "Rate limited. Retry after #{e.retry_after} seconds"
rescue Hanko::AuthenticationError
puts "Invalid API key"
rescue Hanko::ApiError => e
puts "API error: #{e.message} (HTTP #{e.status})"
puts e.body # parsed response body
rescue Hanko::Error => e
puts "Hanko error: #{e.message}"
endSecurity
Algorithm pinning
All JWT verification is pinned to RS256. No algorithm negotiation or none algorithm is accepted.
Credential redaction
Hanko::Client#inspect and Hanko::Configuration#inspect redact the API key, so credentials are never accidentally leaked to logs:
client = Hanko::Client.new(api_url: 'https://example.hanko.io', api_key: 'secret')
puts client.inspect
# => #<Hanko::Client api_url="https://example.hanko.io" api_key=[REDACTED]>JWKS trust
The SDK only fetches JWKS from the configured api_url domain. Webhook verification requires an explicit jwks_url parameter -- the URL is never inferred from untrusted input.
Dependency security
- faraday (~> 2.0) -- HTTP client with middleware support
- jwt (~> 2.7) -- RFC 7519 JWT implementation
- concurrent-ruby (~> 1.0) -- thread-safe data structures
Requirements
- Ruby >= 3.1 (tested on 3.1, 3.2, 3.3, 3.4)
- Rails >= 7.0 (for
hanko-rails, tested on 7.0, 7.1, 7.2, 8.0, 8.1)
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature) - Write tests for your changes
- Ensure all tests pass:
bundle exec rspecin bothhanko/andhanko-rails/ - Ensure linting passes:
bundle exec rubocop - Commit your changes (
git commit -m 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
License
Released under the MIT License.