210 lines
6.7 KiB
TypeScript
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
|
|
// },
|