Integrate NextAuth with Keycloak and implement JWT validation in internal_frontend.
This commit is contained in:
92
internal_frontend/app/api/auth/[...nextauth]/route.ts
Normal file
92
internal_frontend/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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");
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
export {handler as GET, handler as POST};
|
||||
@@ -1,22 +1,27 @@
|
||||
import type {Metadata} from "next";
|
||||
import "./globals.css";
|
||||
import {ThemeProvider} from "@/components/theme-provider";
|
||||
import {SidebarInset, SidebarProvider, SidebarTrigger} from "@/components/ui/sidebar"
|
||||
import {AppSidebar} from "@/components/app-sidebar"
|
||||
import {SidebarInset, SidebarProvider, SidebarTrigger} from "@/components/ui/sidebar";
|
||||
import {AppSidebar} from "@/components/app-sidebar";
|
||||
import React from "react";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {DynamicBreadcrumb} from "@/components/dynamic-breadcrumb";
|
||||
import {getServerSession} from "next-auth";
|
||||
import {authOptions} from "@/app/api/auth/[...nextauth]/route";
|
||||
import LoginScreen from "@/components/login-screen";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Internal | Rhein Software",
|
||||
description: "Internal Tools for Rhein Software Development",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
return (
|
||||
<html lang="de" suppressHydrationWarning>
|
||||
<body>
|
||||
@@ -26,34 +31,38 @@ export default function RootLayout({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar/>
|
||||
<main>
|
||||
<SidebarInset>
|
||||
<header
|
||||
className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1"/>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<DynamicBreadcrumb/>
|
||||
</div>
|
||||
</header>
|
||||
</SidebarInset>
|
||||
{children}
|
||||
</main>
|
||||
</SidebarProvider>
|
||||
{session?.accessToken ? (
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar/>
|
||||
<main>
|
||||
<SidebarInset>
|
||||
<header
|
||||
className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1"/>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<DynamicBreadcrumb/>
|
||||
</div>
|
||||
</header>
|
||||
</SidebarInset>
|
||||
{children}
|
||||
</main>
|
||||
</SidebarProvider>
|
||||
) : (
|
||||
<LoginScreen/>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user