Move OIDC request handlers into @eventuras/fides-auth/server#17
Conversation
There was a problem hiding this comment.
Pull request overview
This PR completes the framework-agnostic @eventuras/fides-auth/server layer by moving OIDC orchestration (login initiation, callback handling, and heartbeat refresh) out of fides-auth-next into core handlers that operate on standard Request/Response plus a CookieStore, leaving Next.js as thin adapters.
Changes:
- Add core server handlers:
handleOidcLogin,handleOidcCallback, andhandleHeartbeat, with orchestration-level tests in@eventuras/fides-auth. - Refactor
@eventuras/fides-auth-nexthandlers into wrappers that injectnextCookieStore()and global GET/POST rate limiters. - Add changeset for the new exports and package version bumps.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/fides-auth/src/server/oidc-login.ts | New framework-agnostic login initiation handler that builds PKCE, stores cookies, and redirects. |
| packages/fides-auth/src/server/oidc-callback.ts | New framework-agnostic callback handler for exchanging code, persisting session, cleaning cookies, and safe redirect. |
| packages/fides-auth/src/server/oidc-callback.test.ts | New tests validating proxy callback-path reconstruction and basic guards. |
| packages/fides-auth/src/server/heartbeat-handler.ts | New framework-agnostic heartbeat handler for refreshing sessions via CookieStore. |
| packages/fides-auth/src/server/heartbeat-handler.test.ts | Updated tests to target the new core heartbeat handler behavior and dependencies. |
| packages/fides-auth/src/server/index.ts | Exposes the new handlers and config types from the server entrypoint. |
| packages/fides-auth-next/src/oidc-login.ts | Replaced Next-specific implementation with a thin wrapper over core handleOidcLogin. |
| packages/fides-auth-next/src/oidc-callback.ts | Replaced Next-specific implementation with a thin wrapper over core handleOidcCallback (but currently drops a previously exported config field). |
| packages/fides-auth-next/src/oidc-callback.test.ts | Removes the Next-side proxy-path regression test (now covered in core). |
| packages/fides-auth-next/src/heartbeat-handler.ts | Replaced Next-specific implementation with a thin wrapper over core handleHeartbeat. |
| .changeset/server-oidc-handlers.md | Changeset describing the move and version bumps. |
Comments suppressed due to low confidence (1)
packages/fides-auth-next/src/oidc-callback.ts:17
OidcCallbackConfigpreviously allowedsessionDurationDays; removing it is a TypeScript-breaking change for consumers importing this type from@eventuras/fides-auth-next. Since session duration options are currently ignored bycreateAndPersistSessionanyway, consider keeping the field for backwards compatibility (and documenting that it is reserved/ignored) rather than removing it in a patch release.
export interface OidcCallbackConfig {
/** OAuth/OIDC configuration (issuer, clientId, clientSecret, redirect_uri, scope) */
oauthConfig: OAuthConfig;
/** Public application URL (used to reconstruct the callback URL and validate redirects) */
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (returnTo) { | ||
| await cookies.set('returnTo', returnTo, defaultOAuthCookieOptions); | ||
| } |
There was a problem hiding this comment.
Fixed in a51545f — when there's no valid returnTo, the handler now deletes any existing returnTo cookie, so an abandoned earlier login can't leak a stale path into the next one's post-login redirect. Covered by a new oidc-login test.
| export async function handleOidcCallback( | ||
| request: Request, | ||
| config: OidcCallbackConfig, | ||
| ): Promise<Response> { | ||
| const { | ||
| oauthConfig, | ||
| applicationUrl, | ||
| rolesClaim = 'roles', | ||
| sessionDurationDays = 7, | ||
| defaultRedirectPath = '/', | ||
| validateReturnTo, | ||
| } = config; | ||
|
|
||
| // 1) Rate limit | ||
| if (!(await globalGETRateLimit())) { | ||
| logger.warn('Rate limit exceeded'); | ||
| return new Response('Too many requests', { status: 429 }); | ||
| } | ||
|
|
||
| try { | ||
| // 2) Reconstruct the public callback URL. Keep the request path: behind a | ||
| // TLS-terminating proxy applicationUrl is just the origin ("/"), and the | ||
| // token-exchange redirect_uri must match the callback path used at authorize. | ||
| const currentUrl = new URL(request.url); | ||
| const publicUrl = new URL(applicationUrl); | ||
| publicUrl.pathname = currentUrl.pathname; | ||
| publicUrl.search = currentUrl.search; | ||
|
|
||
| // 3) Read PKCE cookies | ||
| const storedState = await getAuthCookie('oauth_state'); | ||
| const storedCodeVerifier = await getAuthCookie('oauth_code_verifier'); | ||
|
|
||
| if (!storedState || !storedCodeVerifier) { | ||
| logger.warn('Missing state or code verifier'); | ||
| return new Response('Please restart the login process.', { status: 400 }); | ||
| } | ||
|
|
||
| // 4) Exchange code for tokens (generic OIDC logic in fides-auth) | ||
| const tokens = await exchangeAuthorizationCode( | ||
| oauthConfig, | ||
| publicUrl, | ||
| storedCodeVerifier, | ||
| storedState, | ||
| ); | ||
|
|
||
| // 5) Build session from tokens and persist (split across session/session_at) | ||
| const session = buildSessionFromTokens(tokens, rolesClaim); | ||
| await createAndPersistSession(session, { sessionDurationDays }); | ||
|
|
||
| // 7) Clean up PKCE & returnTo cookies | ||
| const returnTo = await getAuthCookie('returnTo'); | ||
| await deleteAuthCookies(['oauth_state', 'oauth_code_verifier', 'returnTo']); | ||
|
|
||
| // 8) Build safe redirect URL (generic validation in fides-auth) | ||
| const redirectUrl = validateReturnUrl( | ||
| returnTo, | ||
| applicationUrl, | ||
| defaultRedirectPath, | ||
| validateReturnTo, | ||
| ); | ||
| redirectUrl.searchParams.set('login', 'success'); | ||
|
|
||
| logger.info({ redirectUrl: redirectUrl.toString() }, 'Login successful, redirecting'); | ||
|
|
||
| return new Response(null, { | ||
| status: 302, | ||
| headers: { Location: redirectUrl.toString() }, | ||
| }); | ||
| } catch (error) { | ||
| // The session was too large for a cookie — surface this distinctly instead | ||
| // of hiding it in a generic 500, so it's diagnosable in production. | ||
| if (error instanceof CookieTooLargeError) { | ||
| logger.error({ error, cookieName: error.cookieName, size: error.size }, 'Session cookie too large to store'); | ||
| return new Response('The session is too large to store. Please contact support.', { | ||
| status: 431, | ||
| }); | ||
| } | ||
|
|
||
| logger.error({ error }, 'OIDC callback error'); | ||
| return new Response('An unexpected error occurred. Please restart the login process.', { | ||
| status: 500, | ||
| }); | ||
| } | ||
| return coreHandleOidcCallback(request, { | ||
| ...config, | ||
| cookies: await nextCookieStore(), | ||
| rateLimit: globalGETRateLimit, | ||
| }); |
There was a problem hiding this comment.
Good catch — the refactor had actually dropped sessionDurationDays from the Next config, which would break callers passing it. Fixed in a51545f: it's restored on OidcCallbackConfig (documented as reserved/not-yet-applied) and the wrapper now maps fields explicitly so it isn't forwarded to the core handler.
Add handleOidcLogin, handleOidcCallback, and handleHeartbeat to @eventuras/fides-auth/server. Each takes a CookieStore and an optional rate-limit callback and works over the standard Request/Response, so the login, callback, and heartbeat flows are framework-agnostic. The proxy callback-path handling, PKCE cookie lifecycle, session persistence, and response shapes move with them. fides-auth-next's handlers become thin wrappers that supply the Next cookie store (nextCookieStore) and the global GET/POST rate limiters; their public config and behaviour are unchanged. The orchestration tests move to core and drive the handlers directly with an in-memory cookie store.
926d09b to
a51545f
Compare
Why
Completes the framework-agnostic server layer. After the foundation (CookieStore + session helpers, #16), the OIDC request handlers were the last orchestration still living in
fides-auth-next. Their flow — rate limit, PKCE cookie lifecycle, code exchange, session persistence, safe redirect — is identical across frameworks; only cookie/header I/O is Next-specific. Moving them down lets a future React Router adapter reuse the same handlers.What
@eventuras/fides-auth/server— three new handlers over the standardRequest/Response:handleOidcLogin,handleOidcCallback,handleHeartbeatCookieStoreand an optionalrateLimitcallback. The proxy callback-path reconstruction, PKCE cookie cleanup, session persistence (via the existingpersistSession), theCookieTooLargeError→ 431 mapping, and the heartbeat response shape all move with them.@eventuras/fides-auth-next:handleOidcLogin/handleOidcCallback/handleHeartbeatbecome thin wrappers that supplynextCookieStore()and the global GET/POST rate limitersRate limiting stays in
fides-auth-next(it reads the client IP from Next headers) and is injected into the core handlers as a callback.Compatibility
Non-breaking.
@eventuras/fides-authgets a minor (new handler exports),@eventuras/fides-auth-nexta patch. Changeset included.Verification
@eventuras/fides-auth: 221 tests pass, builds.@eventuras/fides-auth-next: 6 tests pass, builds.