Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

authlete/auth-ui

Open more actions menu

Repository files navigation

auth-ui

The login & consent application in the Externalized Login & Consent pattern — paired with authlete/typescript-oauth-server (or any conforming Authlete-backed AS).

Built on Next.js 16 · Tailwind 4 · Better Auth · better-sqlite3.

The AS is the headless half of this pair. auth-ui is the head — every screen a user sees during sign-in or consent is rendered here.

The pattern: Externalized Login & Consent

Intent. Decouple user authentication and consent from the OAuth/OIDC Authorization Server. The AS stays a thin, spec-compliant surface; a separate UI application owns everything the user touches. The AS holds no per-transaction state.

Roles

Component Responsibility What it sees
Relying Party (RP) Initiates /authorize; receives code/tokens. Only the AS.
Authorization Server (AS) OAuth/OIDC endpoints (/authorize, /token, /userinfo, /par, /introspect, /revoke, /jwks, .well-known/*). Delegates user-facing flow to the login app; owns the final redirect back to the RP. RP, Authlete, login app — never external IdPs.
auth-ui (this app) Authenticates the user with any combination of factors (password, MFA, passkeys, federation); collects consent; records the decision against the opaque interaction ticket. Only the opaque ticket id — no codes, no tokens, no RP redirect_uris.
Authlete OAuth/OIDC protocol engine. Owns per-transaction state via tickets. Never reachable from the browser; only the AS calls it.

State model

  • The Authlete ticket is the only handle for an in-flight authorization. Auth result, consent decision, and request context all hang off it on Authlete's side.
  • The AS holds no per-transaction state. The browser carries only the ticket id; the AS exchanges it back for context as needed. (One transitional exception is documented inline in the AS at src/userstore.ts.)
  • auth-ui holds the user session (Better Auth) but not the OAuth transaction. It learns what's being requested via GET /api/interactions/{ticket}, posts back the user's decision, and forwards the user to the AS for finalization.

Trust boundaries

 ┌──────────┐    OAuth / OIDC    ┌──────────┐   component protocol    ┌──────────┐
 │   RP     │ ─────────────────→ │    AS    │ ───(bearer-auth)──────→ │ auth-ui  │
 └──────────┘                    │  (thin)  │                          │(this app)│
                                 │          │   @authlete/sdk          │          │
                                 │          │ ─────────────────→  Authlete         │
                                 └──────────┘                          └─────┬────┘
                                                                             │
                                                  (future) federated IdPs · MFA · passkeys
                                                                             │
                                                                          External
  • AS outward to RPs: standard OAuth/OIDC. One spec.
  • AS ↔ auth-ui: a bespoke 2-endpoint component protocol (GET/POST /api/interactions/{ticket}), bearer-authenticated. auth-ui obtains the bearer via client_credentials + private_key_jwt.
  • auth-ui outward: everything else — federated IdPs, MFA factors, passkeys, account management. None of this is visible to the AS.

Why this pattern

  • Implementation-portable AS. A thin Authlete client with no user state can run as a Node service, a sidecar, a reverse proxy plugin, or live inside an API gateway / edge worker. The same auth-ui works against any of them.
  • Authentication evolves independently. MFA, passkeys, federation, step-up, risk-based prompts — all in auth-ui, none of which the AS ever sees.
  • Consent evolves independently. Granular per-scope/per-claim UI, Rich Authorization Requests (RAR), persistent grant management — all UI work behind the same ticket interface.
  • Independent deploy and scale. Two services, one narrow protocol between them.

This separation matches the architecture Authlete is designed around: the engine owns the spec + per-transaction state; you own the user experience.

What this app currently provides

  • Sign-in / sign-up / forgot-password (Better Auth — email + password today).
  • Consent surface against an in-flight AS ticket.
  • Account self-service: /settings/account, /settings/security via better-auth-ui.
  • Server-to-server client of the AS's component protocol (src/lib/as-client.ts).
  • Server actions that bridge user decisions back to the AS (src/server/interaction-actions.ts).
  • End-to-end smoke harness (scripts/smoke-e2e.mjs).

Run locally

pnpm install
cp .env.example .env
# Fill in BETTER_AUTH_SECRET (32+ chars):   openssl rand -base64 32
# Fill in AS_BASE_URL, AS_CLIENT_ID, AS_CLIENT_PRIVATE_KEY (matching the AS registration)
pnpm dev

Server boots at http://localhost:3001. The AS must be reachable at AS_BASE_URL.

Run the end-to-end smoke against a running AS:

node --env-file=.env scripts/smoke-e2e.mjs

Roadmap

Authentication and consent grow here; the AS does not change for these.

  • MFA (TOTP, WebAuthn second-factor)
  • Passkeys (WebAuthn primary)
  • Magic link
  • Federated sign-in (Google, Microsoft, Okta, custom OIDC IdPs)
  • Richer consent — granular per-claim choices, RAR rendering, persistent grant management
  • Account-recovery and step-up flows

License

Apache-2.0

About

Login and consent UI for Authlete-backed OAuth servers.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Morty Proxy This is a proxified and sanitized view of the page, visit original site.