Skip to content

OIDC provider#1

Merged
rowforce merged 5 commits into
mainfrom
oidc-provider
May 20, 2026
Merged

OIDC provider#1
rowforce merged 5 commits into
mainfrom
oidc-provider

Conversation

@rowforce
Copy link
Copy Markdown
Contributor

No description provided.

rowforce added 5 commits May 20, 2026 20:23
Adds discovery, authorize, token, userinfo, and end-session endpoints
so off-the-shelf OIDC libraries (next-auth, passport-openidconnect,
Spring Security, etc.) can authenticate against ManyRows by pointing
at the per-app issuer URL — no ManyRows SDK required. Coexists with
the existing AppKit SDK path; existing customers unaffected.

- Per-app issuer at {AuthDomain or BASE_URL}/x/{slug}/apps/{appId}
- PKCE required (S256 only; 43-char code_challenge length validated;
  43-128 char code_verifier enforced per RFC 7636 §4.1)
- Exact-match redirect_uri + post_logout_redirect_uri allowlists
- Atomic code consume; replay-revoke per OIDC §3.1.3.2
- Cross-check session.user_id == code.user_id at /token
- access_token iss = host-only (SDK-compatible); id_token iss =
  per-app path (matches discovery, spec MUST)
- Confidential + public client modes; constant-time secret compare
- email_verified always present alongside email (spec §5.1)
- /oidc/login and authorize error pages have X-Frame-Options + CSP
  frame-ancestors to defend credential entry against clickjacking
- OIDC requires cookie transport mode (validated at admin config
  time + defense-in-depth check at /authorize runtime)
- end-session accepts both GET and POST per OIDC Session Mgmt §5

Schema (00002_oidc_provider.sql, additive):
- oidc_auth_codes: short-lived authorization-code grants
- oidc_pending_authorize: carries the /authorize request across
  the AppKit-sign-in round trip
- four columns on apps: oidc_enabled, oidc_client_secret_hash,
  oidc_redirect_uris, oidc_post_logout_redirect_uris

Janitor sweeps both new tables; codes and pending rows linger past
use for a 1-hour grace window so replay detection sees them.

21 tests covering discovery, authorize parameter validation, both
grant types, PKCE mismatch, code reuse + session revoke, userinfo
bearer validation, end-session (GET + POST + allowlist), transport-
mode validation (admin + runtime), anti-clickjacking headers,
malformed code_challenge rejection, and a full RP end-to-end test
that drives discovery -> JWKS fetch -> authorize -> token ->
id_token signature verification against the JWKS.
Phase 2 — makes the OIDC provider actually configurable from the
admin UI. Without this, enabling OIDC on an app required raw SQL.

Admin API (PUT/GET /admin/.../apps/{id}/oidc-config):
- Server-generated client_secret with shown-once response shape
  (matches the api_keys "generate -> show once -> hash" pattern)
- regenerateSecret + clearSecret are mutually exclusive
- Enabling with no redirect_uris rejected (error.oidcRedirectUrisRequired)
- Enabling on local-transport-mode app rejected via the existing
  ErrOIDCRequiresCookieTransport from the repo layer
- Tri-state slice semantics: omit field = no change, [] = clear,
  [...] = replace

Admin UI (new "OIDC" tab in AppAuthMethods):
- Enable toggle with cookie-transport-mode warning when not set
- Read-only Discovery URL / Issuer URL / Client ID with copy buttons
- Generate/rotate client_secret with show-once dialog ("I've saved it")
- Clear-secret downgrade-to-public with confirmation dialog
- Redirect URIs allowlist editor (chip list + add/remove)
- Post-logout redirect URIs allowlist editor (same shape)
- All values validated as http(s) URLs before adding

6 new admin handler tests + the earlier 21 OIDC protocol tests
still all pass (27 total). UI passes tsc + vite build.
UX fix: the Regenerate/Clear client_secret buttons each issued their
own targeted PUT (with only the secret flag, no URI fields), which
silently bypassed any unsaved redirect-URI edits in the form. Local
state then carried changes the server never saw, with no save-or-
discard prompt. Both buttons are now disabled when the form is
dirty, with a Tooltip pointing at the save bar.

Test fix: new TestAdminOIDCConfig_DisableAfterEnable proves the
ordinary "turn it off" path AND that the client_secret hash and
redirect URIs survive disable — so re-enabling later does not
require re-running secret distribution.

7 admin tests pass (was 6). 28 OIDC tests total, full repo green.
Adds OIDC to the "What you get" feature list and a dedicated
"Integrating via OpenID Connect" section between the AppKit section
and Architecture. Covers:

- Where to configure (App -> Auth methods -> OIDC)
- The three values an RP library needs (discovery URL, client ID,
  client secret) and how to obtain them
- A next-auth code snippet pointing at the discovery URL
- The cookie-transport-mode requirement, called out as a note
- That OIDC coexists with the AppKit SDK in parallel

Documentation only — no behavioural changes.
Audit pass #3 caught: the custom clearOIDCSessionCookies hardcoded
Secure=true, SameSite=Lax, and omitted Domain entirely. Browsers
require clear-cookie attributes to MATCH the set-cookie attributes —
so for apps with cookie_domain configured, or in dev mode, or with
SameSite=Strict, the browser kept the stale cookie in its jar.
Server-side the session was already revoked, so this wasn't a
security hole; just a correctness/UX bug where dead cookies
persisted and 401'd subsequent requests until natural expiry.

Fix: use the existing handler.clearSessionCookies(w, ws, app) which
threads resolveCookieDomain + resolveSameSite + IsDevMode-aware
Secure. Drop the custom helper.

Plus a regression test: sets cookie_domain on the app, drives
/oidc/end-session, asserts the Set-Cookie has the matching Domain.
The old end-session test didn't check Set-Cookie attributes at all
— exactly the gap that let this slip through earlier audits.

22 OIDC protocol tests + 7 admin tests pass (was 21+7).
@rowforce rowforce merged commit 4e98083 into main May 20, 2026
@rowforce rowforce deleted the oidc-provider branch May 20, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant