Usher — a lightweight single-tenant OpenID Provider in Common Lisp

1 branch

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-smtp for 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 via config :db (:backend :sqlite|:postgres ...); TOTP secrets, backup codes, and email/reset tokens live in a usher_credentials table
  • 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.