Neah/app/api/auth/[...nextauth]/route.ts.bak
2025-05-02 13:01:33 +02:00

309 lines
8.8 KiB
TypeScript

import NextAuth, { NextAuthOptions } from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
import { jwtDecode } from "jwt-decode";
interface KeycloakProfile {
sub: string;
email?: string;
name?: string;
roles?: string[];
preferred_username?: string;
given_name?: string;
family_name?: string;
realm_access?: {
roles: string[];
};
}
interface DecodedToken {
realm_access?: {
roles: string[];
};
[key: string]: any;
}
declare module "next-auth" {
interface Session {
user: {
id: string;
name?: string | null;
email?: string | null;
image?: string | null;
username: string;
first_name: string;
last_name: string;
role: string[];
nextcloudInitialized?: boolean;
};
accessToken?: string;
}
interface JWT {
sub?: string;
accessToken?: string;
refreshToken?: string;
accessTokenExpires?: number;
role?: string[];
username?: string;
first_name?: string;
last_name?: string;
error?: string;
email?: string | null;
name?: string | null;
}
}
function getRequiredEnvVar(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
async function refreshAccessToken(token: JWT) {
try {
console.log('Attempting to refresh access token');
const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.KEYCLOAK_CLIENT_ID!,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
}),
method: "POST",
});
const refreshedTokens = await response.json();
if (!response.ok) {
console.error('Token refresh failed with status:', response.status);
console.error('Error response:', refreshedTokens);
throw refreshedTokens;
}
console.log('Token refresh successful');
return {
...token,
accessToken: refreshedTokens.access_token,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
error: undefined, // Clear any previous errors
};
} catch (error) {
console.error("Error refreshing access token:", error);
return {
...token,
error: "RefreshAccessTokenError",
};
}
}
export const authOptions: NextAuthOptions = {
providers: [
KeycloakProvider({
clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"),
clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"),
issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"),
authorization: {
params: {
scope: "openid profile email roles"
}
},
profile(profile) {
// Simplified profile logging to reduce console noise
console.log('Keycloak profile received');
// Get roles from realm_access
const roles = profile.realm_access?.roles || [];
// Clean up roles by removing ROLE_ prefix and converting to lowercase
const cleanRoles = roles.map((role: string) =>
role.replace(/^ROLE_/, '').toLowerCase()
);
return {
id: profile.sub,
name: profile.name ?? profile.preferred_username,
email: profile.email,
first_name: profile.given_name ?? '',
last_name: profile.family_name ?? '',
username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '',
role: cleanRoles,
}
},
}),
],
session: {
strategy: "jwt",
maxAge: 12 * 60 * 60, // Reduce to 12 hours to help with token size
},
cookies: {
sessionToken: {
name: `next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'none',
path: '/',
secure: true,
domain: process.env.NEXTAUTH_COOKIE_DOMAIN || undefined,
maxAge: 12 * 60 * 60, // Match session maxAge
},
},
},
jwt: {
// Maximum JWT size to prevent chunking
maxAge: 12 * 60 * 60, // Reduce to 12 hours
},
callbacks: {
async jwt({ token, account, profile }) {
// Initial sign in
if (account && profile) {
const keycloakProfile = profile as KeycloakProfile;
const roles = keycloakProfile.realm_access?.roles || [];
// Only include admin, owner, user roles (most critical)
const criticalRoles = roles
.filter(role =>
role.includes('admin') ||
role.includes('owner') ||
role.includes('user')
)
.map(role => role.replace(/^ROLE_/, '').toLowerCase());
// Store absolute minimal data in the token
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
token.accessTokenExpires = account.expires_at ?? 0;
token.sub = keycloakProfile.sub;
token.role = criticalRoles.length > 0 ? criticalRoles : ['user']; // Only critical roles
token.username = keycloakProfile.preferred_username?.substring(0, 30) ?? '';
token.error = undefined;
// Don't store first/last name in the token to save space
// Applications can get these from the userinfo endpoint if needed
return token;
}
// Check if token has expired
const tokenExpiresAt = token.accessTokenExpires ? token.accessTokenExpires as number : 0;
const currentTime = Date.now();
const hasExpired = currentTime >= tokenExpiresAt;
// If the token is still valid, return it
if (!hasExpired) {
return token;
}
// If refresh token is missing, force sign in
if (!token.refreshToken) {
console.warn('No refresh token available, session cannot be refreshed');
return {
...token,
error: "RefreshAccessTokenError"
};
}
// Try to refresh the token
const refreshedToken = await refreshAccessToken(token as JWT);
// If there was an error refreshing, mark token for re-authentication
if (refreshedToken.error) {
console.warn('Token refresh failed, user will need to reauthenticate');
return {
...refreshedToken,
error: "RefreshAccessTokenError"
};
}
return refreshedToken;
},
async session({ session, token }) {
try {
// Handle the error from jwt callback
if (token.error === "RefreshAccessTokenError") {
console.warn("Session encountered a refresh token error, redirecting to login");
// Return minimal session with error flag that will trigger re-auth in client
return {
...session,
error: "RefreshTokenError",
user: {
id: token.sub ?? '',
role: ['user'], // Default role
username: '', // Empty username
first_name: '',
last_name: '',
name: null,
email: null,
image: null,
nextcloudInitialized: false,
}
};
}
const userRoles = Array.isArray(token.role) ? token.role : [];
// Create an extremely minimal user object
session.user = {
id: token.sub ?? '',
email: token.email ?? null,
name: token.name ?? null,
image: null,
username: token.username ?? '',
first_name: token.first_name ?? '',
last_name: token.last_name ?? '',
role: userRoles,
// Don't include nextcloudInitialized or other non-essential fields
};
// Only store access token, not the entire token
session.accessToken = token.accessToken;
return session;
} catch (error) {
console.error("Error in session callback:", error);
// Return minimal session with error flag
return {
...session,
error: "SessionError",
user: {
id: token.sub ?? '',
role: ['user'],
username: '',
first_name: '',
last_name: '',
name: null,
email: null,
image: null,
nextcloudInitialized: false,
}
};
}
}
},
pages: {
signIn: '/signin',
error: '/signin',
},
debug: false, // Disable debug to reduce cookie size from logging
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
interface JWT {
accessToken: string;
refreshToken: string;
accessTokenExpires: number;
}
interface Profile {
sub?: string;
email?: string;
name?: string;
roles?: string[];
}