From fe516b794580f3ddfa43a2588bb52ca893952209 Mon Sep 17 00:00:00 2001 From: Leonardo Rivera Date: Wed, 12 Mar 2025 10:44:44 -0400 Subject: [PATCH 1/3] refresh keycloak access token --- global/utils/keycloakUtils.ts | 44 ++++++++++++++++++++++++++++++++- pages/api/auth/[...nextauth].ts | 31 ++++++++++++++--------- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/global/utils/keycloakUtils.ts b/global/utils/keycloakUtils.ts index 80c809ec..a6c02226 100644 --- a/global/utils/keycloakUtils.ts +++ b/global/utils/keycloakUtils.ts @@ -1,6 +1,13 @@ import { getConfig } from '@/global/config'; +import urlJoin from 'url-join'; -const { NEXT_PUBLIC_KEYCLOAK_PERMISSION_AUDIENCE } = getConfig(); +const { + NEXT_PUBLIC_KEYCLOAK_CLIENT_ID, + KEYCLOAK_CLIENT_SECRET, + NEXT_PUBLIC_KEYCLOAK_PERMISSION_AUDIENCE, + NEXT_PUBLIC_KEYCLOAK_HOST, + NEXT_PUBLIC_KEYCLOAK_REALM, +} = getConfig(); export const permissionBodyParams = () => { return new URLSearchParams({ @@ -21,3 +28,38 @@ export const scopesFromPermissions = (permissions: Permission[]) => { .filter(({ scopes }) => scopes) .flatMap(({ rsname, scopes }) => scopes.flatMap((scope) => [rsname + '.' + scope])); }; + +export const refreshAccessToken = async (refreshToken: string) => { + try { + const url = urlJoin( + NEXT_PUBLIC_KEYCLOAK_HOST, + `realms`, + NEXT_PUBLIC_KEYCLOAK_REALM, + 'protocol/openid-connect/token', + ); + + const formData = new URLSearchParams({ + client_id: NEXT_PUBLIC_KEYCLOAK_CLIENT_ID, + client_secret: KEYCLOAK_CLIENT_SECRET, + grant_type: 'refresh_token', + refresh_token: refreshToken, + }); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, + body: formData, + }); + + // Parse the response and return the result + if (response.ok) { + return await response.json(); + } + return; + } catch (error) { + console.error('Error during token refresh:', error); + return; + } +}; diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 71069384..00ff53f4 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -10,7 +10,7 @@ import { getConfig } from '@/global/config'; import { KEYCLOAK_URL_ISSUER, AUTH_PROVIDER, KEYCLOAK_URL_TOKEN } from '@/global/utils/constants'; import { decodeToken, extractUser } from '@/global/utils/egoTokenUtils'; import { encryptContent } from '@/global/utils/crypt'; -import { permissionBodyParams, scopesFromPermissions } from '@/global/utils/keycloakUtils'; +import { permissionBodyParams, refreshAccessToken, scopesFromPermissions } from '@/global/utils/keycloakUtils'; const { NEXT_PUBLIC_KEYCLOAK_CLIENT_ID, @@ -20,10 +20,7 @@ const { NEXT_PUBLIC_EGO_CLIENT_ID, } = getConfig(); -const egoLoginUrl = urlJoin( - NEXT_PUBLIC_EGO_API_ROOT, - `/oauth/ego-token?client_id=${NEXT_PUBLIC_EGO_CLIENT_ID}`, -); +const egoLoginUrl = urlJoin(NEXT_PUBLIC_EGO_API_ROOT, `/oauth/ego-token?client_id=${NEXT_PUBLIC_EGO_CLIENT_ID}`); const fetchEgoToken = async (login_nonce: string) => { const { data } = await axios.post(egoLoginUrl, null, { @@ -45,9 +42,7 @@ export const fetchScopes = async (accessToken: string) => { return data ? scopesFromPermissions(data) : []; }; -export const getAuthOptions = ( - req: GetServerSidePropsContext['req'] | NextApiRequest, -): AuthOptions => { +export const getAuthOptions = (req: GetServerSidePropsContext['req'] | NextApiRequest): AuthOptions => { return { secret: SESSION_ENCRYPTION_SECRET, // Configure one or more authentication providers @@ -99,21 +94,33 @@ export const getAuthOptions = ( callbacks: { async jwt({ token, user, account, profile, trigger }: any) { if (trigger === 'signIn') { - if (account?.provider == AUTH_PROVIDER.EGO) { + const provider = account?.provider; + if (provider === AUTH_PROVIDER.EGO) { token.account = account; token.profile = user; - } else if (account?.provider == AUTH_PROVIDER.KEYCLOAK) { + } else if (provider === AUTH_PROVIDER.KEYCLOAK) { token.account = account; token.profile = profile; token.scopes = await fetchScopes(token.account.access_token); } + } else { + const tokenExpiresAtMs = token.account.expires_at * 1000; + if (Date.now() > tokenExpiresAtMs) { + // Access token has expired. Use the refresh token to obtain a new one. + const requestedNewToken = await refreshAccessToken(token.account.refresh_token); + token.account = { + ...token.account, + ...requestedNewToken, + }; + } } return token; }, async session({ token, session }: any) { // Send properties to the client, like an access_token and user id from a provider. - if (token.account.provider == AUTH_PROVIDER.EGO) { + const provider = token.account.provider; + if (provider === AUTH_PROVIDER.EGO) { const { egoToken, scope, ...profileWithoutEgoToken } = token.profile; session.account = { accessToken: encryptContent(egoToken), @@ -121,7 +128,7 @@ export const getAuthOptions = ( }; session.scopes = scope; session.user = { ...profileWithoutEgoToken }; - } else if (token.account.provider == AUTH_PROVIDER.KEYCLOAK) { + } else if (provider === AUTH_PROVIDER.KEYCLOAK) { session.account = { accessToken: encryptContent(token?.account?.access_token), provider: token?.account?.provider, From 976c1dc647e4b33b3745bf4536984333c67cb041 Mon Sep 17 00:00:00 2001 From: Leonardo Rivera Date: Wed, 12 Mar 2025 12:41:27 -0400 Subject: [PATCH 2/3] refactoring code --- global/utils/keycloakUtils.ts | 2 -- pages/api/auth/[...nextauth].ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/global/utils/keycloakUtils.ts b/global/utils/keycloakUtils.ts index a6c02226..bf2f30ad 100644 --- a/global/utils/keycloakUtils.ts +++ b/global/utils/keycloakUtils.ts @@ -57,9 +57,7 @@ export const refreshAccessToken = async (refreshToken: string) => { if (response.ok) { return await response.json(); } - return; } catch (error) { console.error('Error during token refresh:', error); - return; } }; diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 00ff53f4..2f047c81 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -105,7 +105,7 @@ export const getAuthOptions = (req: GetServerSidePropsContext['req'] | NextApiRe } } else { const tokenExpiresAtMs = token.account.expires_at * 1000; - if (Date.now() > tokenExpiresAtMs) { + if (Date.now() >= tokenExpiresAtMs) { // Access token has expired. Use the refresh token to obtain a new one. const requestedNewToken = await refreshAccessToken(token.account.refresh_token); token.account = { From d9f6d62af7acaf12ca47af12be3f3b235998a26e Mon Sep 17 00:00:00 2001 From: Leonardo Rivera Date: Fri, 21 Mar 2025 14:30:34 -0400 Subject: [PATCH 3/3] keycloak refresh access token --- pages/api/auth/[...nextauth].ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 2f047c81..fc370596 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -104,14 +104,16 @@ export const getAuthOptions = (req: GetServerSidePropsContext['req'] | NextApiRe token.scopes = await fetchScopes(token.account.access_token); } } else { - const tokenExpiresAtMs = token.account.expires_at * 1000; - if (Date.now() >= tokenExpiresAtMs) { - // Access token has expired. Use the refresh token to obtain a new one. - const requestedNewToken = await refreshAccessToken(token.account.refresh_token); - token.account = { - ...token.account, - ...requestedNewToken, - }; + if (account?.provider === AUTH_PROVIDER.KEYCLOAK) { + const tokenExpiresAtMs = token.account.expires_at * 1000; + if (Date.now() >= tokenExpiresAtMs) { + // Access token has expired. Use the refresh token to obtain a new one. + const requestedNewToken = await refreshAccessToken(token.account.refresh_token); + token.account = { + ...token.account, + ...requestedNewToken, + }; + } } }