honin-client
A framework-agnostic OAuth2/PKCE client gem for apps authenticating against a Honin identity provider — Honin Cloud (honin.id), self-hosted instances, or any app running the honin engine in IDP mode.
Works with Rails, Sinatra, Hanami, or any Rack app. If you are already using OmniAuth, use omniauth-honin instead — it builds on this gem.
Installation
gem "honin-client"
Configuration
HoninClient.configure do |c|
c.idp_url = "https://honin.id" # default
c.client_id = ENV["HONIN_CLIENT_ID"]
c.client_secret = ENV["HONIN_CLIENT_SECRET"]
c.redirect_uri = "https://myapp.com/auth/callback"
c.scope = "email profile" # default
end
Self-hosted instance (sub-path mount)
If the Honin engine is mounted under a path — e.g. mount HoninEngine, at: "/auth" — set base_path. It is used to construct the OAuth endpoints and verify the iss claim in the JWT.
HoninClient.configure do |c|
c.idp_url = "https://id.acme.co"
c.base_path = "/auth"
# ...
end
Not needed for Honin Cloud or root-mounted instances.
Usage
Rails (opt-in concern)
# config/initializers/honin_client.rb
require "honin/client/rails"
HoninClient.configure do |c|
c.client_id = ENV["HONIN_CLIENT_ID"]
c.client_secret = ENV["HONIN_CLIENT_SECRET"]
c.redirect_uri = "https://myapp.com/auth/callback"
end
# app/controllers/application_controller.rb
include HoninClient::Rails::Authentication
# config/routes.rb
get "/auth/login", to: "auth#login"
get "/auth/callback", to: "auth#callback"
delete "/auth/logout", to: "auth#logout"
# app/controllers/auth_controller.rb
class AuthController < ApplicationController
def login
redirect_to , allow_other_host: true
end
def callback
identity = handle_honin_callback
redirect_to root_path, notice: "Signed in as #{identity.email}"
rescue HoninClient::Error => e
redirect_to root_path, alert: "Sign in failed: #{e.}"
end
def logout
sign_out_honin
redirect_to root_path
end
end
# protect any controller action
before_action :require_honin_authentication
The concern provides:
| Method | Description |
|---|---|
current_honin_identity |
Returns the signed-in Identity, or nil |
honin_identity_signed_in? |
Boolean |
require_honin_authentication |
Redirects to Honin if not signed in |
honin_authorization_url |
Generates the IDP redirect URL, stores PKCE state in session |
handle_honin_callback |
Verifies state, exchanges code, stores identity in session, returns Identity |
sign_out_honin |
Clears identity from session |
Any framework (core API)
require "honin/client"
# 1. Start the flow — redirect the user to Honin
pkce = HoninClient::PKCE.new
session[:honin_pkce_verifier] = pkce.code_verifier
session[:honin_state] = SecureRandom.hex(16)
url = HoninClient.flow.(
state: session[:honin_state],
code_challenge: pkce.code_challenge
)
redirect url
# 2. Handle the callback
identity = HoninClient.flow.exchange_code(
code: params[:code],
code_verifier: session[:honin_pkce_verifier]
)
session[:honin_identity] = identity.to_h
Identity
exchange_code and handle_honin_callback both return a HoninClient::Identity:
identity.sub # "abc123xyz456def789" — stable user ID, use as foreign key
identity.account_type # "standard" or "anonymous"
identity.email # nil for anonymous or if scope not granted
identity.email_verified # true / false / nil
identity.display_name # nil if "profile" scope not granted
identity.timezone # nil if "timezone" scope not granted
identity.locale # nil if "locale" scope not granted
identity.granted_scopes # ["email", "profile"]
identity.required_scopes # ["email"]
identity.standard? # true
identity.anonymous? # false
identity["custom_claim"] # access any JWT claim by name
identity.to_h # full claims hash — safe to store in session
Restore from session:
HoninClient::Identity.new(session[:honin_identity])
JWT verification
By default, exchange_code fetches the JWKS from <idp_url>/.well-known/jwks.json and verifies the RS256 signature. Keys are cached in memory for one hour and refreshed automatically on key rotation (unknown kid).
To use a pre-loaded key set instead (e.g. for a self-hosted instance with a known public key):
jwk_set = JWT::JWK::Set.new([JWT::JWK.new(your_public_key)])
cache = HoninClient::JwksCache::Static.new(jwk_set)
verifier = HoninClient::TokenVerifier.new(
jwks_cache: cache,
issuer: HoninClient.configuration.issuer,
client_id: HoninClient.configuration.client_id
)
identity = verifier.verify(raw_jwt)
Scopes
Honin supports email, profile, timezone, and locale. Scopes must be registered on your Honin Application. The default scope is "email profile".
HoninClient.configure do |c|
c.scope = "email profile timezone locale"
end
How it works
PKCE.newgenerates a cryptographically random code verifier and its S256 challenge.- The user is redirected to the Honin authorize endpoint with the challenge and a random state.
- Honin authenticates the user, collects consent, and redirects back with an authorization code.
exchange_codePOSTs the code + verifier to/oauth/tokenusingclient_secret_post.- Honin verifies PKCE, issues a signed RS256 JWT. The JWT is verified via JWKS and returned as an
Identity.
No userinfo endpoint — all claims are in the JWT.
Development
bundle install
bundle exec rake test
bundle exec standardrb
License
MIT — see LICENSE.