fp/services/next/app/lib/auth.ts

210 lines
6.7 KiB
TypeScript

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<JWT> {
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
// },