Skip to content

Feature: Minimal OpenID Connect (OIDC) Provider — Auth Code + PKCE #2

Description

@hoangsonww

Summary
Expose Passwordless-Auth as a first-party OIDC Identity Provider so apps can use standard OIDC/OAuth2 instead of bespoke JWT exchange. MVP supports Authorization Code + PKCE, RS256/ES256-signed ID tokens, discovery (.well-known), JWKS, and userinfo.

Why

  • Works with off-the-shelf OIDC clients (NextAuth, Keycloak adapters, Spring, Auth.js, etc.).
  • Keeps the server passwordless (magic link / WebAuthn / TOTP) while presenting a standard OAuth layer to relying parties.
  • Enables multi-app SSO, token introspection, and gradual ecosystem integration.

Scope (MVP)

Endpoints

  • GET /.well-known/openid-configuration
  • GET /.well-known/jwks.json (serve active + previous JWKs; include kid)
  • GET /oauth/authorize (consent + auth code issue; PKCE required)
  • POST /oauth/token (code → tokens; refresh support; returns id_token, access_token, refresh_token)
  • GET /oauth/userinfo (email, email_verified, sub, name?)

Supported grant/response

  • Authorization Code with PKCE (S256 only)
  • Response types: code
  • Scopes: openid (required), email, profile, offline_access (maps to refresh token)

Tokens & claims

  • ID Token: iss, sub (stable UUID), aud, iat, exp, auth_time, nonce, email, email_verified
  • Access Token: opaque or JWT (JWT is fine for now; sign with same key)
  • Refresh Token: existing mechanism reused; tie to client_id

Crypto / keys

  • Switch to asymmetric signing for OIDC: ES256 (preferred) or RS256.
  • Persist a JWK keystore (SQLite table) with kid, alg, created_at, expires_at, active.
  • Key rotation CLI: passwordless-auth keys rotate → publishes new JWK while keeping last one for validation.
  • .well-known/jwks.json exposes active + last non-expired keys.

Clients & consent

  • Static client registry (config + DB): client_id, hashed client_secret (optional for public clients), redirect_uris[], allowed_scopes[], first_party.
  • Consent screen (HTML minimal) or auto-consent for first_party=true.
  • Enforce exact redirect_uri match (no wildcards).

Flows (UX)

  1. RP sends user to /oauth/authorize?client_id=...&redirect_uri=...&scope=openid%20email&response_type=code&state=...&code_challenge=...&code_challenge_method=S256&nonce=...
  2. If not authenticated, server drives passwordless login (magic link/WebAuthn/TOTP).
  3. Issue short-lived auth code (e.g., 60–120s), bind to code_challenge, client_id, redirect_uri, nonce.
  4. Token exchange at /oauth/token with code_verifier returns tokens.
  5. RP may call /oauth/userinfo.

Config (TOML)

[oidc]
enable = true
issuer = "https://auth.example.com"
require_pkce = true
id_token_ttl_seconds = 900
access_token_ttl_seconds = 900
refresh_token_ttl_seconds = 604800

[oidc.keys]
alg = "ES256"            # or "RS256"
jwks_store = "jwks.db"   # or reuse sqlite

[[oidc.clients]]
client_id = "myapp-web"
first_party = true
redirect_uris = ["https://myapp.example.com/callback"]
allowed_scopes = ["openid","email","profile","offline_access"]
public_client = true     # PKCE required, no client_secret

Acceptance Criteria

  • /.well-known/openid-configuration & /.well-known/jwks.json pass basic OIDC discovery checks.
  • End-to-end Auth Code + PKCE works with a sample OIDC client (e.g., Auth.js / NextAuth) using openid email.
  • id_token includes correct aud,nonce,email_verified; signature verifies against JWKS.
  • Exact redirect_uri matching; state echoed; nonce validated.
  • Auth code single-use, short TTL; replay rejected.
  • PKCE enforced (S256 only); plain rejected.
  • Key rotation does not break existing tokens; JWKS serves both old/new until old expiry.
  • Rate limiting on /oauth/authorize + /oauth/token to mitigate abuse.
  • Unit + integration tests (happy path, bad code_verifier, wrong redirect_uri, expired code, JWKS kid swap).

Security Notes

  • Asymmetric signing is mandatory for OIDC (don’t expose jwt_secret).
  • Enforce HTTPS in production; verify origin/rp_id alignment for WebAuthn step.
  • Short code TTL; bind code to client_id + redirect_uri + code_challenge.
  • Strict CORS on /oauth/* as needed; CSRF not applicable to GET auth but keep best practices on consent POST if any.
  • Store client_secrets salted/hashed (if using confidential clients).

Out of Scope (v1)

  • Dynamic client registration, PAR, Device Code, Back-channel logout, JWT Secured Authorization Request, mTLS, Federation.
  • Admin UI (CLI + config only for now).

Migration / Docs

  • New oidc section in config.toml.
  • Add docs/oidc.md with copy-paste client configs (NextAuth, Spring Security, OAuth2 Proxy).
  • Example: example/oidc-nextjs/ minimal app for manual verification.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationduplicateThis issue or pull request already existsenhancementNew feature or requestgood first issueGood for newcomershelp wantedExtra attention is neededquestionFurther information is requested

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions