feat(auth): force re-auth on failed refresh
requireAuth() now redirects on user.error so SSR catches stale sessions
that the cookie-presence middleware can't see. New SessionExpirationGuard
(client) listens on useSession() and calls signIn("keycloak") when the
JWT carries RefreshAccessTokenError or RefreshTokenMissing. Without it,
a tab idle past the Keycloak SSO lifetime sat on a dead accessToken
until a downstream API 401'd, with no UI-level redirect.
Bumps to 0.4.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
src/auth/SessionExpirationGuard.tsx
Normal file
50
src/auth/SessionExpirationGuard.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useSession, signIn } from "next-auth/react";
|
||||
|
||||
import type { SessionUser } from "./types";
|
||||
|
||||
/**
|
||||
* Drop-in client component that re-triggers Keycloak sign-in when the
|
||||
* kit's server-side refresh flow gives up (`session.user.error` set to
|
||||
* `RefreshAccessTokenError` or `RefreshTokenMissing`).
|
||||
*
|
||||
* Without this, a UI tab left open past the Keycloak SSO session lifetime
|
||||
* holds a stale `accessToken`: the middleware sees a cookie, `requireAuth`
|
||||
* sees an `accessToken` string, and the user only finds out things are
|
||||
* broken when a downstream API returns 401. Mount this inside a
|
||||
* `<SessionProvider>` (typically alongside other providers in the locale
|
||||
* root) so the session refetch interval surfaces the error within minutes.
|
||||
*
|
||||
* Renders nothing.
|
||||
*/
|
||||
export function SessionExpirationGuard() {
|
||||
const { data: session } = useSession();
|
||||
const error = (session?.user as Partial<SessionUser> | undefined)?.error;
|
||||
// Guard against React's StrictMode double-invoke + the brief window
|
||||
// between calling signIn and the page navigating away.
|
||||
const firedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error || firedRef.current) return;
|
||||
if (
|
||||
error !== "RefreshAccessTokenError" &&
|
||||
error !== "RefreshTokenMissing"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
firedRef.current = true;
|
||||
const callbackUrl =
|
||||
typeof window !== "undefined"
|
||||
? window.location.pathname + window.location.search
|
||||
: "/";
|
||||
// signIn() POSTs the CSRF-protected provider endpoint, which then
|
||||
// initiates a fresh OIDC flow; the stale session cookie is replaced
|
||||
// atomically when the flow completes. Keycloak's own SSO cookie may
|
||||
// still log the user in transparently if it hasn't also expired.
|
||||
void signIn("keycloak", { callbackUrl });
|
||||
}, [error]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user