feat: migrate auth from clerk to keycloak #13

Merged
jalenluorion merged 11 commits from feat/keycloak-auth into main 2026-09-29 01:03:11 +00:00
Member

Moves auth from Clerk to ScottyLabs Keycloak (via Ricochet) and verifies tokens on every API route.

Before deploy: set SESSION_SECRET and SCRAPER_API_TOKEN in prod OpenBao, run migration b41f7c2d9e10, and test a real CMU login.

Generated with Claude Code

Moves auth from Clerk to ScottyLabs Keycloak (via Ricochet) and verifies tokens on every API route. Before deploy: set SESSION_SECRET and SCRAPER_API_TOKEN in prod OpenBao, run migration b41f7c2d9e10, and test a real CMU login. Generated with [Claude Code](https://claude.com/claude-code)
The API trusted a client-supplied Clerk-User-Id header, user_id query
parameters and clerk_id/user_id JSON fields, so anyone could act as any
user, and 30 of 31 mutating routes checked nothing at all.

- A single before_request hook verifies `Authorization: Bearer` tokens
  against the scottylabs realm: RS256 signature via the cached JWKS
  (rate-limited refetch on unknown kid), iss, exp, azp/aud = the OIDC
  client, typ = Bearer. g.user is the only source of identity.
- Routes are protected by default; @public opts out (health, test_db for
  the keepalive, event and org browsing, the Google OAuth redirects).
- Mutating routes are authorized: own schedules and saved events only;
  org-scoped actions need an admin/manager row for that org (category
  scope respected); global actions need PROJECT_ADMIN_GROUP.
- The SOC scraper may call regenerate_occurrences_by_events with
  SCRAPER_API_TOKEN; every other route rejects it.
- First Keycloak login links an existing user by email (or the Andrew
  UPN) and sets users.oidc_sub, else creates the user. clerk_id is kept.
- Additive migration adds users.oidc_sub with a unique constraint. It
  revises 55da73f9050c, which the backend port PR adds.
- Tests cover token forgery, client-supplied ids, and 401/403 on every
  mutating route, on an in-memory SQLite copy of the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Next.js now runs the OIDC authorization code flow (PKCE, nonce) with
openid-client, following the housing and cmugpt-surface pattern: the
redirect_uri is the Ricochet relay and the real callback rides in the
state envelope as return_to.

- /api/auth/login, /callback, /token and /logout route handlers.
- The session is an encrypted httpOnly cookie holding the refresh token;
  the browser only receives short-lived access tokens from /api/auth/token
  and sends them to the API as a bearer token via axios interceptors.
- Expired tokens are refreshed server-side; a 401 with a Bearer challenge
  is retried once with a fresh token.
- Pages render per request so signed-in state is never prerendered.
- Remove @clerk/nextjs, the middleware, the Clerk env vars and every
  client-sent user id. Update npmDepsHash for the new lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore: declare keycloak secrets and run ricochet in devenv
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
e92564d3f5
Declare the OIDC settings governance already writes to OpenBao, plus
SESSION_SECRET for the web session cookies and the optional
SCRAPER_API_TOKEN. Drop CLERK_SECRET_KEY. Enable the local Ricochet relay
on 127.0.0.1:8090 for devenv users.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jalenluorion force-pushed feat/keycloak-auth from e92564d3f5
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
to 0151e41de3
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
2026-09-28 23:49:16 +00:00
Compare
jalenluorion force-pushed feat/keycloak-auth from 0151e41de3
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
to b7176ec493
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
2026-09-28 23:51:29 +00:00
Compare
fix(api): link keycloak logins by andrew id like other scottylabs services
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
4b4580082f
Real scottylabs-realm access tokens carry email_verified=false, so requiring
a verified email refused every sign-in. Follow the other ScottyLabs services
(housing, tartan-vote, courses, quest, study), which identify users by sub
and take the Andrew ID from preferred_username without consulting
email_verified.

That trust comes from the realm: only the CMU identity provider can create
accounts (cmu-dev, slack, google and cmu-saml are link-only), it admits only
@andrew.cmu.edu principals and auto-links CMU's LDAP, and users cannot edit
their username or email. Clerk-era rows are linked once by the CMU address
in the token or the Andrew UPN; non-CMU addresses are never used to link,
and a row already linked to another account is still never taken over.

A valid token that maps to no user now falls back to the anonymous view on
public routes instead of failing them. Invalid or expired tokens still get
401 so the web client refreshes.
refactor(api): use PyJWKClient instead of a custom JWKS cache
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
0997806fcf
Drop the hand-rolled key cache and settings class, the unused PROJECT_GROUP,
and tests that duplicated others or only exercised PyJWT itself.
jalenluorion force-pushed feat/keycloak-auth from 0997806fcf
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
to af2c4a3176
Some checks failed
kennel/build build succeeded
CI / check (pull_request) Has been cancelled
CI / check-1 (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
2026-09-29 00:32:15 +00:00
Compare
fix(api): send a user agent when fetching keycloak signing keys
Some checks failed
kennel/build build succeeded
CI / check-1 (pull_request) Successful in 10m6s
CI / build (pull_request) Failing after 31m12s
CI / check (pull_request) Failing after 0s
547ebae1ca
Cloudflare in front of idp.scottylabs.org answers urllib's default
User-Agent with 403, so every token check failed with 503.
jalenluorion deleted branch feat/keycloak-auth 2026-09-29 01:03:11 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ScottyLabs/cal!13
No description provided.