/auth/login + /auth/callback + /auth/logout endpoints: OIDC code exchange + SLO (consumes #14) #16
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type
Feature
Lineage
Decomposed from
forgejo_admin/westside-admin#2(Keycloak cookie SSR auth + admin role gate). Sub-task 3 of 4. Depends onforgejo_admin/westside-admin#14(keycloak.ts lib) — must land first. Independent of sub-task 2 (hooks.server.ts).Repo
forgejo_admin/westside-adminUser Story
story-westside-admin-admin-row-crud. These endpoints are the OIDC protocol surface — they're the only routes that interact directly with Keycloak's/protocol/openid-connect/{auth,token,logout}endpoints. They sit OUTSIDE the admin-role gate (the user must be allowed to log in before they have a role).Context
/auth/login(GET) builds the Keycloak/authorizeURL with PKCE + a fresh OIDCstate(#14'sgenerateOidcState), stores the signed state in a transient HttpOnly cookie, and 302s the browser there. The transient state cookie hasMax-Age=600(10-minute auth window) andPath=/auth/callback(only sent to the callback)./auth/callback(GET) reads the transient state cookie, calls #14'sverifyOidcStateagainst thestatequery param before any token exchange (CSRF prevention), then POSTs thecodeto Keycloak's/protocol/openid-connect/tokenendpoint withgrant_type=authorization_code+ the PKCEcode_verifier+ confidential-client basic auth (KEYCLOAK_CLIENT_ID:KEYCLOAK_CLIENT_SECRET). On success, encrypts the tokens via #14'sencryptCookiePayload, sets the session cookie, deletes the transient state cookie, redirects to the originalredirectquery param (or/)./auth/logout(POST) clears the session cookie, then 302s the browser to Keycloak's/protocol/openid-connect/logoutendpoint (RP-initiated logout / SLO) so the user is logged out of all realm clients in the same session.File Targets
Create:
src/routes/auth/login/+server.ts— GET only, builds Keycloak/authorizeURL, sets transient state cookie, 302.src/routes/auth/callback/+server.ts— GET only, validates state THEN exchanges code, sets session cookie, 302 to original target.src/routes/auth/logout/+server.ts— POST only, clears session cookie, 302 to Keycloak logout endpoint.Do NOT touch:
src/lib/server/keycloak.ts(sub-task 1, frozen).src/hooks.server.ts(sub-task 2 — the hook redirects HERE but doesn't define these endpoints).src/routes/(unauthorized)/*(sub-task 4 — orthogonal page).Acceptance Criteria
/auth/login: on GET, generates fresh state via #14, sets transient cookiewestside_admin_statewithHttpOnly; Secure; SameSite=Lax; Path=/auth/callback; Max-Age=600, 302s to Keycloak/authorizeURL withclient_id,redirect_uri=https://westside-admin.tail5b443a.ts.net/auth/callback,response_type=code,scope=openid profile email,state={state},code_challenge_method=S256,code_challenge={pkce}./auth/callback: on GET with mismatched state, returns HTTP 400 with bodystate_mismatch. Token exchange must NOT have run./auth/callback: on GET with missing state cookie, returns HTTP 400 with bodystate_missing. (Replay/late-callback hardening.)/auth/callback: on GET with valid state, POSTs to Keycloak/token, on success encrypts tokens via #14'sencryptCookiePayload, setswestside_admin_sessioncookie (HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age={refresh_expiry}), clears the transient state cookie, 302 toredirectquery param (or/if absent)./auth/callback: Keycloak/tokenfailure (4xx/5xx response, network error) returns HTTP 502 with a generic body — never echoes the Keycloak error body to the client./auth/logout: POST clearswestside_admin_session(set withMax-Age=0), 302s to${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/logout?post_logout_redirect_uri=https://westside-admin.tail5b443a.ts.net/&id_token_hint={id_token}./auth/logout: also accepts GET? No — POST only, with CSRF protection via SvelteKit form actions or explicit origin check./tokenresponse bodies in any log line.Test Expectations
keycloak.tail5b443a.ts.net):/auth/loginin browser → expect Keycloak login form. Confirmstateandcode_challengequery params present./auth/callback?code=...&state=...→ expect 302 to/. Confirmwestside_admin_sessioncookie set withHttpOnlyandSecure.statequery param manually before submission → expect 400./auth/logoutPOST → expect cookie cleared + 302 to Keycloak logout endpoint.npm run dev+ manual browser flow.Constraints
fetchfor the token POST. Do NOT pullaxios/got.event.cookiesAPI exclusively.code_verifiergeneration: usecrypto.randomBytes(32).toString('base64url')inline in/auth/login. Thesrc/lib/server/keycloak.tslibrary is frozen at the #14 merge — do NOT add a new export for PKCE generation. Thecode_challengederives from the verifier via SHA-256 + base64url, also inline.code_verifiermust be stored alongside the state in the transient cookie (so callback can retrieve it) — encrypt the whole{state, code_verifier}blob via #14'sencryptCookiePayloadfor the transient cookie, OR sign + concat. Choose one and document the call in code.https://westside-admin.tail5b443a.ts.net/auth/callback(matches Keycloak client config — exact match enforced by Keycloak).https://westside-admin.tail5b443a.ts.net/(matches "Valid post-logout redirect URIs" in Keycloak client config).Checklist
feedback_funnel_requires_authRelated
project-westside-adminforgejo_admin/westside-admin#2— parentforgejo_admin/westside-admin#14— keycloak.ts lib (DEPENDS ON)sop-keycloak-client-creation— defines the redirect URIs and state requirementfeedback_funnel_requires_authScope Review: APPROVED
Review note:
review-1136-2026-05-03Scope is solid. All three primitives the body references (
generateOidcState,verifyOidcState,encryptCookiePayload) exist onmainafter the #14/#18 merge. File targets verified absent (no collisions). Env vars landed viapal-e-deployments#147. Keycloak client config matches the body's exact redirect URIs. Within the 5-minute rule (3 thin RequestHandler files in one repo, 7 ACs that collapse to 3 endpoints + 4 callback branches).Two minor
[BODY]nits — non-blocking, fix in flight or trust the dev:code_verifieris generated inline in+server.tsbut never says so. One sentence ("PKCE generation is not exported from keycloak.ts; derivecode_verifier+code_challengeinline via Node'scrypto.randomBytes+ base64url + SHA-256") prevents the dev from inventing a new export on the frozen lib.One carry-forward
[SCOPE]flag fromreview-1132-2026-05-03:arch-keycloakumbrella note still missing across the admin-row-crud chain. Not a per-ticket blocker; Ava holds the call on whether to backfill.Cleared to advance to
todo./auth/callback + /auth/logout endpoints: OIDC code exchange + SLO (consumes #14)to /auth/login + /auth/callback + /auth/logout endpoints: OIDC code exchange + SLO (consumes #14)