import KeycloakProvider, { type KeycloakProfile } from "next-auth/providers/keycloak"; import { NextAuthOptions } from "next-auth"; import { jwtDecode } from "jwt-decode"; import { type JWT } from "next-auth/jwt"; import { User, Profile } from "next-auth"; import { configs } from "../config/configs"; import { getPatreonMemberships, getKeycloakIdpToken, updateKeycloakUserPatreonEntitlements, tiersToRolesMap } from "./patreon"; import { getTierNameFromTierId } from "./keycloak"; declare module "next-auth" { interface Session { user: User; token: JWT | undefined; profile: Profile; } interface Profile { realm_access: any; preferred_username: string; username_visibility?: "public" | "private"; } interface Session { roles: any } } declare module "next-auth/jwt" { interface JWT { expires_at: number; access_token: string; refresh_token: string; profile: Profile; } } export const authOptions: NextAuthOptions = { session: { strategy: "jwt" }, providers: [ KeycloakProvider({ clientId: configs.keycloakClientId, clientSecret: configs.keycloakClientSecret, issuer: configs.keycloakIssuer }) ], callbacks: { async jwt({ token, account, profile }): Promise { if (account) { // First-time login, save the `access_token`, its expiry and the `refresh_token` const decodedAccountToken = jwtDecode(account.access_token as any) as KeycloakProfile return { ...token, access_token: account.access_token, expires_at: account.expires_at, refresh_token: account.refresh_token, client_roles: decodedAccountToken.realm_access.roles, scope: decodedAccountToken.scope, resource_access: decodedAccountToken.resource_access, profile: profile, } } else if (Date.now() < token.expires_at * 1000) { // Subsequent logins, but the `access_token` is still valid return token } else { // Subsequent logins, but the `access_token` has expired, try to refresh it if (!token.refresh_token) throw new TypeError("Missing refresh_token") try { console.log(`>>> we are refreshing the token`) const response = await fetch(`https://keycloak.fp.sbtp.xyz/realms/futureporn/protocol/openid-connect/token`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: configs.keycloakClientId, client_secret: configs.keycloakClientSecret, refresh_token: token.refresh_token, }).toString(), }) const tokensOrError = await response.json() if (!response.ok) throw tokensOrError const newTokens = tokensOrError as { access_token: string expires_in: number refresh_token?: string } token.access_token = newTokens.access_token token.expires_at = Math.floor( Date.now() / 1000 + newTokens.expires_in ) // Some providers only issue refresh tokens once, so preserve if we did not get a new one if (newTokens.refresh_token) { token.refresh_token = newTokens.refresh_token } return token } catch (error) { console.error("Error refreshing access_token", error) // If we fail to refresh the token, return an error so we can handle it on the page token.error = "RefreshTokenError" return token } } }, // async jwt({ token, account, profile }) { // if (account) { // const decodedAccountToken = jwtDecode(account.access_token as any) as KeycloakProfile // // console.log(`jwt() callback. decodedAccountToken as follows.`) // // console.log(decodedAccountToken) // token.client_roles = decodedAccountToken.realm_access.roles // token.scope = decodedAccountToken.scope // token.resource_access = decodedAccountToken.resource_access; // token.expires_at = account.expires_at ?? 0; // token.access_token = account.access_token!; // token.refresh_token = account.refresh_token!; // } // if (profile) { // token.profile = profile // } // return token; // }, async session({ session, token, trigger }) { // console.log(`Token interceptor to add token info to the session to use on the pages. trigger=${trigger}`) // console.log(JSON.stringify(token, null, 2)) // console.log('session as follows') // console.log(JSON.stringify(session, null, 2)) // get user's patreon tiers and adds them to the appropriate keycloak group const entitledTiers = await updateKeycloakUserPatreonEntitlements(token) // console.log(`entitledTiers=${JSON.stringify(entitledTiers)}`) const entitledTierName = getTierNameFromTierId(entitledTiers[0]) const entitledRoles = tiersToRolesMap[entitledTierName] console.log(`entitledRoles=${entitledRoles.join(', ')}`) const userId = token?.sub if (!userId) throw new Error('failed to get userId from token.sub'); // session.account = token.account session.profile = token.profile session.roles = entitledRoles // this is a hack to avoid the user having to log in twice to get synced with keycloak roles session.token = token return session } } } // async jwt({ token, account, profile }) { // try { // if (account) { // const decodedToken = jwtDecode(account.access_token as any) // if (token == null){ // throw new Error("Unable to decode token") // } // console.log('decodedToken as follows') // console.log(JSON.stringify(decodedToken, null, 2)) // // Do something here to add more info, maybe just overwrite profile (thats the one that should have this info) // profile = decodedToken // token.account = account // } // if (profile) { // console.log('profile as follows') // console.log(profile) // token.profile = profile // // Then do here the assignation of roles elements to token so session has access // // This can be modified so uses by client, realm or account BE AWARE OF THAT! // // Modify the "resource_access['next-auth-AFB']" value to the one your resource/realm/accout // // json scope roles you need // // While the info is already on profile, we could make a new key on the json response of session // const clientRoles = profile.realm_access.roles // token.client_roles = clientRoles // } // } catch (error) { // console.log(error) // } // return token // },