Skip to content

Repository files navigation

auth-ui

White-label authentication & consent UI for an Authlete-backed OAuth/OIDC Authorization Server (e.g. authlete/typescript-oauth-server). The AS is headless; auth-ui renders every screen the user sees during authentication and consent.

Built on Next.js 16 · Tailwind 4 · Better Auth · libSQL.

Quickstart

Standalone auth app — sign-in/up, 2FA, account settings. No AS, no cloud services, no manual steps (libSQL is just a file in dev):

npm install
npm run setup   # .env + generated secrets + local SQLite schema
npm run dev     # → http://localhost:3001

As the OAuth consent UI — pair with a running typescript-oauth-server (default http://localhost:3000, with its AUTH_UI_URL pointing here):

  1. AS_ISSUER_ID in .env is the AS's origin — the default already matches a local AS.
  2. AUTH_UI_JWKS is the signing key the AS verifies — npm run setup already generated it.
  3. End-to-end check (set AS_BASE_URL to the AS origin for the script):
node --env-file=.env scripts/smoke-e2e.mjs

The two servers talk server-to-server (per-request mutual JWT), so they must be network-reachable to each other. The protocol spec is INTERACTION_PROTOCOL.md in the AS repo.

Configuration

.env is grouped by mode (see .env.example). npm run setup fills the two generated secrets — you rarely touch these by hand.

Variable Purpose
BETTER_AUTH_SECRET session secret — generated by npm run setup (or npm run keygen)
BETTER_AUTH_URL this app's URL (default http://localhost:3001)
DATABASE_URL libSQL target — a file locally, a Turso URL when hosted
AS_ISSUER_ID (consent mode) the AS's stable identity — and the origin its public JWKS is fetched from to verify its JWTs
AUTH_UI_JWKS (consent mode) this app's ES256 private JWKS — generated by npm run setup; public half published at /.well-known/jwks.json

The runtime learns the AS's per-request callback URL from the interaction token, not from config. AS_BASE_URL is read only by the smoke-e2e.mjs check, to tell it which AS origin to drive.

Optional features

  • Email — with no RESEND_API_KEY, auth emails (verify, password reset) are logged to the console, so local dev needs no provider. Set the key to send for real; EMAIL_FROM needs a verified domain in prod.
  • Social / OIDC sign-in — a provider's button appears only when both <PROVIDER>_CLIENT_ID and <PROVIDER>_CLIENT_SECRET are set (works for any Better Auth built-in). Register the callback URL {BETTER_AUTH_URL}/api/auth/callback/<provider> with the provider.

Deploy (hosted)

Vercel-ready; Next.js is auto-detected (no vercel.json). The only requirement is a network database — serverless has no persistent local disk — so use a free Turso libSQL database. Same code path as local, just a remote target.

Turso (same steps as local, remote target):

  • create a Turso account (free) and a database
  • put its URL + token in .env as DATABASE_URL (libsql://…) and DATABASE_AUTH_TOKEN
  • npm run migrate to populate the schema

Vercel env vars: DATABASE_URL, DATABASE_AUTH_TOKEN, BETTER_AUTH_SECRET, BETTER_AUTH_URL (your deploy URL), and — for consent mode — AS_ISSUER_ID, AUTH_UI_JWKS. Mark the secrets Sensitive. Optional feature keys (email, social) live in .env.example.

The interaction protocol is server-to-server, so a hosted auth-ui needs an AS it can reach over the network (and the AS must reach it back) — a hosted auth-ui can't pair with a localhost AS.

White-label

Ships unbranded. Rebrand from one file:

  • src/brand/brand.ts is the single source of truth — product name, logo, font, colors, sign-in panel copy. Colors flow into CSS variables; nothing else hardcodes a brand value.
  • Set logoMark to an image in /public/brand, or keep the built-in neutral mark.
  • Consent wording — scope and permission labels — lives in src/lib/consent-labels.ts, one file to localize or relabel.
  • Or replace the UI entirely — anything that speaks the same protocol to the AS works.

What you own vs what's managed

Most files in this repo are managed — installed from the better-auth-ui registry (built on shadcn/ui primitives). The code that is actually this project is a thin layer around them:

Path Status
src/components/auth/, src/components/ui/ Managed — registry-installed; don't hand-edit, update by re-installing
src/lib/auth/ Mixed — registry plugins + a few owned hooks
src/brand/ Owned — the white-label surface (see below)
everything else in src/ Owned — Better Auth config, email transport, and the AS interaction protocol (lib/jws, lib/jwks, lib/as-client, app/authorizations, app/apps, app/api/users)

Update managed code by re-installing, never by editing:

npx shadcn@latest add @better-auth-ui/<name>   # better-auth-ui: auth, two-factor, …
npx shadcn@latest add <name>                    # shadcn/ui primitives: button, dialog, …

New features (2FA, passkeys, social, …) come from the registry: install the component, wire it in config, never fork it.

How it works — Externalized Authentication & Consent

Decouples authentication and consent from the AS. The AS stays a thin, spec-compliant OAuth/OIDC surface holding no per-transaction state; auth-ui owns everything the user touches.

Component Role
RP the app requesting access: starts /authorize, receives tokens. Integrates with the AS using standard OAuth/OIDC.
AS OAuth/OIDC endpoints; delegates authentication/consent to auth-ui; owns the redirect back to the RP
auth-ui the UI the user actually sees: authenticates the user, collects consent, records the decision against an opaque authorization id
Authlete protocol engine; owns per-transaction state; only the AS calls it

auth-ui holds the user session (Better Auth), not the OAuth transaction. It speaks a small component protocol to the AS, authenticated by per-request mutual JWT — each side publishes a JWKS and verifies the other:

  • GET /api/authorizations/{id} — fetch the in-flight authorization (auth-ui → AS)
  • POST /api/authorizations/{id}/outcome — report an interaction outcome: authenticate, then consent (auth-ui → AS)
  • GET /api/users/{id} — resolve user claims (AS → auth-ui)
  • GET /.well-known/jwks.json — auth-ui's public keys (AS → auth-ui)

Beyond the auth/consent transaction, auth-ui also renders a connected-apps screen (/apps) to review and revoke granted access, over an AS management API (same mutual JWT):

  • GET /api/authorized-apps — list a user's granted apps (auth-ui → AS)
  • DELETE /api/authorized-apps/{clientId} — revoke an app's access (auth-ui → AS)

Why: the AS stays implementation-portable (Node service, sidecar, gateway, edge worker), while authentication (MFA, passkeys, federation) and consent (per-claim, RAR, grant management) evolve entirely in auth-ui — none of which the AS ever sees.

Roadmap

Supported today:

  • From the registry: email/password, email verification, password reset, multi-account device sessions, TOTP 2FA + backup codes, env-gated social / OIDC sign-in (Google, Microsoft, …).
  • Consent: RAR consent capture (renders authorization_details) and a connected-apps panel (/apps) to review and revoke granted access.

Planned:

  • Passkeys (WebAuthn) · Magic link · custom-issuer OIDC (Okta/Auth0 via discovery)
  • Richer consent — Persistent grant lifecyle management (RAR consent capture shipped)
  • Account recovery / step-up

License

Apache-2.0

About

Login and consent UI for Authlete-backed OAuth servers.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages