Usher — a lightweight single-tenant OpenID Provider in Common Lisp
README.md
Usher
A lightweight, single-tenant OpenID Provider (OP) written in Common Lisp.
Usher authenticates humans and issues signed OIDC tokens that relying-party applications verify. It exists to replace heavyweight identity providers (Keycloak) where a JVM, realms, federation, and an admin console are unjustified.
It runs as a single SBCL process backed by PostgreSQL (or an in-memory store for
development/tests), terminates TLS 1.3 in-process via
pure-tls with automatic Let's Encrypt,
and is administered from the REPL/CLI rather than a web console.
The full design rationale and security model live in docs/design.md.
Status
Early MVP. Implemented and tested:
- RS256 signing keys + self-serialized JWKS
- ID / access token issuance and validation (alg-confusion–safe; full claim checks)
- Argon2i password hashing (PHC strings; constant-time verify; dummy-hash on miss)
- PKCE (S256)
- Authorization-code grant — validate client + PKCE before consuming the code, with replay detection
- Refresh-token rotation with reuse detection + family revocation
- OIDC discovery,
/authorize(login form),/token,/userinfo - Login rate-limiting + account lockout (bounds brute-force and caps Argon2i CPU/mem)
- TOTP two-factor (RFC 6238) —
otpauth://enrollment URI for QR apps, enroll/confirm, second-factor login step, and single-use backup/recovery codes (/totp/enroll,/totp/confirm,/totp/backup-codes) - Email verification and password reset — single-use hashed tokens +
/verify-email//reset-password, pluggable mail backend (capture for dev/tests,cl-smtpfor prod) - Social login (identity brokering) for GitHub and Google — Usher
acts as an OAuth/OIDC relying party to the upstream, links/provisions a local
account (by upstream sub, then verified email), then issues its own tokens
(
/social/<provider>/start+/social/<provider>/callback); configure with:social ((:key "github" :client-id .. :client-secret ..) ...) - Pluggable store protocol with three backends: in-memory (dev/tests),
SQLite (
cl-sqlite), and PostgreSQL (postmodern) — select viaconfig :db (:backend :sqlite|:postgres ...); TOTP secrets, backup codes, and email/reset tokens live in ausher_credentialstable - In-process TLS 1.3 (
pure-tls) and a plain-HTTP mode for development
Quick start
;; deps: ocicl install
(asdf:load-system :usher)
(in-package :usher)
(setf *config* (make-default-config :issuer "http://localhost:8099"
:listen-port 8099 :tls '(:mode :none)))
(let ((store (make-memory-store)))
(store-add-client store (make-client :id "app" :type :public
:redirect-uris '("http://localhost/cb")))
(store-add-user store (make-user :id 1 :subject "sub-alice" :username "alice"
:name "Alice" :status "active"
:password-hash (hash-password "pw")))
(start :provider (make-provider :store store
:signing-keys (list (generate-signing-key)))))
Then browse http://localhost:8099/.well-known/openid-configuration.
For production TLS, use (:mode :pure-tls-acme :acme-contact "you@example.com")
and set :issuer/:listen-port to your public HTTPS host on port 443
(see docs/design.md §8.1 for ACME/TLS-ALPN-01 requirements).
Embedding in another app
Usher can run inside an existing web app instead of standalone — depend on
usher/core and call its functions from your own routes (framework-agnostic),
or, for Hunchentoot apps, mount the bundled handlers. The route surface is
introspectable: (usher:routes) / (usher:print-routes). See
docs/embedding.md for the full guide, route table, and a
Clack skeleton.
Theming the auth pages
The server-rendered pages (login, two-factor, password reset, email
verification) share one HTML shell and carry stable usher-* class hooks, so a
host app can brand them without Usher owning the visual design. Three config
keys (all optional) drive it:
(usher:make-default-config
:brand "Acme" ; shown in <title>
:head-html "<link rel=\"stylesheet\" href=\"/static/auth.css\">" ; injected into <head>
:body-class "theme-acme") ; appended to <body>
Then style against the class hooks in your own stylesheet: usher-auth
(body), usher-card, usher-title, usher-text, usher-error, usher-form,
usher-field, usher-label, usher-input, usher-submit, usher-social.
With all three unset the markup is unchanged from the built-in default.
Develop
make deps # ocicl install
make build # compile the full server system
make test # run the test suite (no DB or network needed)
make http-smoke # in-process end-to-end HTTP check
License
MIT — see LICENSE.