179 lines
5.1 KiB
TypeScript
179 lines
5.1 KiB
TypeScript
import { NextAuthOptions } from 'next-auth';
|
|
import KeycloakProvider from 'next-auth/providers/keycloak';
|
|
|
|
declare module 'next-auth' {
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
name?: string | null;
|
|
role: string[];
|
|
first_name: string;
|
|
last_name: string;
|
|
username: string;
|
|
}
|
|
interface Session {
|
|
user: {
|
|
id: string;
|
|
email: string;
|
|
name?: string | null;
|
|
role: string[];
|
|
first_name: string;
|
|
last_name: string;
|
|
username: string;
|
|
};
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
}
|
|
interface Profile {
|
|
sub?: string;
|
|
email?: string;
|
|
name?: string;
|
|
roles?: string[];
|
|
given_name?: string;
|
|
family_name?: string;
|
|
preferred_username?: string;
|
|
}
|
|
}
|
|
|
|
declare module 'next-auth/jwt' {
|
|
interface JWT {
|
|
id: string;
|
|
email: string;
|
|
name?: string;
|
|
role: string[];
|
|
first_name: string;
|
|
last_name: string;
|
|
username: string;
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
accessTokenExpires: number;
|
|
error?: string;
|
|
}
|
|
}
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
providers: [
|
|
KeycloakProvider({
|
|
clientId: process.env.KEYCLOAK_CLIENT_ID!,
|
|
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
|
issuer: process.env.KEYCLOAK_ISSUER,
|
|
authorization: {
|
|
params: {
|
|
scope: 'openid email profile',
|
|
response_type: 'code',
|
|
}
|
|
},
|
|
}),
|
|
],
|
|
debug: true,
|
|
session: {
|
|
strategy: 'jwt',
|
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
},
|
|
pages: {
|
|
signIn: '/signin',
|
|
error: '/signin',
|
|
},
|
|
callbacks: {
|
|
async jwt({ token, account, profile }) {
|
|
console.log('JWT callback:', {
|
|
tokenBefore: { ...token, refreshToken: token.refreshToken ? '[REDACTED]' : undefined },
|
|
account: account ? { ...account, refresh_token: '[REDACTED]' } : null,
|
|
profile
|
|
});
|
|
|
|
if (account && profile) {
|
|
if (!profile.sub) {
|
|
console.error('No user ID (sub) provided by Keycloak');
|
|
throw new Error('No user ID (sub) provided by Keycloak');
|
|
}
|
|
if (!account.access_token || !account.refresh_token || !account.expires_at) {
|
|
console.error('Missing required token fields from Keycloak');
|
|
throw new Error('Missing required token fields from Keycloak');
|
|
}
|
|
token.id = profile.sub;
|
|
token.email = profile.email || '';
|
|
token.name = profile.name;
|
|
token.role = profile.roles || ['user'];
|
|
token.first_name = profile.given_name || '';
|
|
token.last_name = profile.family_name || '';
|
|
token.username = profile.preferred_username || '';
|
|
token.accessToken = account.access_token;
|
|
token.refreshToken = account.refresh_token;
|
|
token.accessTokenExpires = account.expires_at * 1000;
|
|
|
|
console.log('JWT token updated:', {
|
|
tokenAfter: { ...token, refreshToken: '[REDACTED]' }
|
|
});
|
|
}
|
|
|
|
// Return previous token if not expired
|
|
if (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
|
|
return token;
|
|
}
|
|
|
|
// Token expired, try to refresh
|
|
if (!token.refreshToken) {
|
|
console.error('No refresh token available');
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
try {
|
|
console.log('Attempting to refresh token...');
|
|
const response = await fetch(
|
|
`${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'refresh_token',
|
|
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
|
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
|
refresh_token: token.refreshToken,
|
|
}),
|
|
}
|
|
);
|
|
|
|
const tokens = await response.json();
|
|
|
|
if (!response.ok) {
|
|
console.error('Token refresh failed:', tokens);
|
|
throw new Error('RefreshAccessTokenError');
|
|
}
|
|
|
|
console.log('Token refreshed successfully');
|
|
return {
|
|
...token,
|
|
accessToken: tokens.access_token,
|
|
refreshToken: tokens.refresh_token ?? token.refreshToken,
|
|
accessTokenExpires: Date.now() + tokens.expires_in * 1000,
|
|
};
|
|
} catch (error) {
|
|
console.error('Error refreshing token:', error);
|
|
return {
|
|
...token,
|
|
error: 'RefreshAccessTokenError',
|
|
};
|
|
}
|
|
},
|
|
async session({ session, token }) {
|
|
if (token.error) {
|
|
throw new Error('RefreshAccessTokenError');
|
|
}
|
|
|
|
session.user.id = token.id;
|
|
session.user.email = token.email;
|
|
session.user.name = token.name;
|
|
session.user.role = token.role;
|
|
session.user.first_name = token.first_name;
|
|
session.user.last_name = token.last_name;
|
|
session.user.username = token.username;
|
|
session.accessToken = token.accessToken;
|
|
session.refreshToken = token.refreshToken;
|
|
|
|
return session;
|
|
},
|
|
},
|
|
};
|