internal frontend implementation with keycloak authentication #16
@@ -1,92 +1,5 @@
|
||||
import NextAuth from "next-auth";
|
||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||
import type {NextAuthOptions} from "next-auth";
|
||||
|
||||
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} = token as TypedJWT;
|
||||
if (access_token) {
|
||||
const valid = await isTokenValid(access_token);
|
||||
if (!valid) {
|
||||
console.warn("[auth] Access token invalid — clearing session");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[auth] JWT callback: reusing existing token");
|
||||
return token;
|
||||
},
|
||||
|
||||
async session({session, token}) {
|
||||
const {access_token, refresh_token} = token as TypedJWT;
|
||||
console.log("[auth] Session callback: enriching session with tokens");
|
||||
return {
|
||||
...session,
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
console.log("[auth] NextAuth handler initialized");
|
||||
import {authOptions} from "@/lib/auth/authOptions";
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
export {handler as GET, handler as POST};
|
||||
|
||||
86
internal_frontend/lib/auth/authOptions.ts
Normal file
86
internal_frontend/lib/auth/authOptions.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||
import type {NextAuthOptions} from "next-auth";
|
||||
|
||||
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} = token as TypedJWT;
|
||||
if (access_token) {
|
||||
const valid = await isTokenValid(access_token);
|
||||
if (!valid) {
|
||||
console.warn("[auth] Access token invalid — clearing session");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[auth] JWT callback: reusing existing token");
|
||||
return token;
|
||||
},
|
||||
|
||||
async session({session, token}) {
|
||||
const {access_token, refresh_token} = token as TypedJWT;
|
||||
console.log("[auth] Session callback: enriching session with tokens");
|
||||
return {
|
||||
...session,
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user