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>
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
"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;
|
|
}
|