Files
Thatsaphorn Atchariyaphap bdbaf36456 Centralize authentication logic and integrate token refresh mechanism
- 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`.
2025-07-11 23:42:41 +02:00

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,
};
},
},
};