- Introduce `AuthWrapper` component for streamlined session-based layouts and authentication handling. - Add new utilities (`tokenUtils.ts`) for JWT decoding, token expiration checks, and refresh operations via Keycloak. - Refactor `serverCall` and `authOptions` to use centralized token refresh logic, removing redundant implementations. - Implement `ClientSessionProvider` for consistent session management across the client application. - Simplify `RootLayout` by delegating authentication enforcement to `AuthWrapper`.
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import KeycloakProvider from "next-auth/providers/keycloak";
|
|
import type {NextAuthOptions} from "next-auth";
|
|
import {getAccessToken} from "@/lib/api/auth/tokenUtils";
|
|
|
|
interface TypedJWT {
|
|
access_token?: string;
|
|
refresh_token?: string;
|
|
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
const {
|
|
KEYCLOAK_CLIENT_ID,
|
|
KEYCLOAK_CLIENT_SECRET,
|
|
KEYCLOAK_ISSUER,
|
|
NEXTAUTH_SECRET,
|
|
} = process.env;
|
|
|
|
if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID");
|
|
if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET");
|
|
if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER");
|
|
if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET");
|
|
|
|
// console.log("[auth] Using Keycloak provider:");
|
|
// console.log(" - Client ID:", KEYCLOAK_CLIENT_ID);
|
|
// console.log(" - Issuer:", KEYCLOAK_ISSUER);
|
|
|
|
async function isTokenValid(token: string): Promise<boolean> {
|
|
try {
|
|
const res = await fetch(`${KEYCLOAK_ISSUER}/protocol/openid-connect/userinfo`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
return res.ok;
|
|
} catch (error) {
|
|
console.error("[auth] Failed to validate access token:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
providers: [
|
|
KeycloakProvider({
|
|
clientId: KEYCLOAK_CLIENT_ID,
|
|
clientSecret: KEYCLOAK_CLIENT_SECRET,
|
|
issuer: KEYCLOAK_ISSUER,
|
|
}),
|
|
],
|
|
secret: NEXTAUTH_SECRET,
|
|
session: {
|
|
strategy: "jwt",
|
|
},
|
|
callbacks: {
|
|
async jwt({token, account}) {
|
|
if (account) {
|
|
token.access_token = account.access_token;
|
|
token.refresh_token = account.refresh_token;
|
|
console.log("[auth] JWT callback: new login from Keycloak");
|
|
return token;
|
|
}
|
|
|
|
const {access_token, refresh_token} = token as TypedJWT;
|
|
if (access_token) {
|
|
// Use centralized getAccessToken function
|
|
const tokenResult = await getAccessToken(access_token, refresh_token);
|
|
|
|
if (tokenResult.accessToken) {
|
|
token.access_token = tokenResult.accessToken;
|
|
if (tokenResult.refreshToken) {
|
|
token.refresh_token = tokenResult.refreshToken;
|
|
}
|
|
|
|
if (tokenResult.refreshed) {
|
|
console.log("[auth] Token refreshed successfully in JWT callback");
|
|
return token;
|
|
}
|
|
|
|
// If token wasn't refreshed, fall back to network validation
|
|
const valid = await isTokenValid(tokenResult.accessToken);
|
|
if (!valid) {
|
|
console.warn("[auth] Access token invalid — clearing session");
|
|
return {};
|
|
}
|
|
} else {
|
|
console.warn("[auth] No valid access token available — clearing session");
|
|
return {};
|
|
}
|
|
}
|
|
|
|
return token;
|
|
},
|
|
|
|
async session({session, token}) {
|
|
const {access_token, refresh_token} = token as TypedJWT;
|
|
return {
|
|
...session,
|
|
accessToken: access_token,
|
|
refreshToken: refresh_token,
|
|
};
|
|
},
|
|
},
|
|
}; |