From d65c4fe787c0b33417d1a2608923d5b1af90015c Mon Sep 17 00:00:00 2001 From: Pascal Kaufmann Date: Sat, 4 Jan 2025 23:57:54 +0100 Subject: [PATCH] Fix logout and use jwt parsing instead of live call to userinfo --- examples/keycloak/boot.ts | 73 +++++++++++++++---------------- packages/api/src/api-index.ts | 2 +- packages/api/src/context.ts | 2 - packages/api/src/fastify/index.ts | 9 ++-- 4 files changed, 39 insertions(+), 47 deletions(-) diff --git a/examples/keycloak/boot.ts b/examples/keycloak/boot.ts index 190a0a4e3..d1473bd33 100644 --- a/examples/keycloak/boot.ts +++ b/examples/keycloak/boot.ts @@ -6,8 +6,10 @@ import { createLogger } from '@unchainedshop/logger'; import seed from './seed.js'; import Fastify, { FastifyInstance, FastifyRequest } from 'fastify'; import FastifyOAuth2 from '@fastify/oauth2'; -import { Context, UnchainedContextResolver } from '@unchainedshop/api'; +import { Context, UnchainedContextResolver, API_EVENTS } from '@unchainedshop/api'; import fastifyCookie from '@fastify/cookie'; +import { emit } from '@unchainedshop/events'; +import jwt from 'jsonwebtoken'; const logger = createLogger('keycloak'); @@ -42,7 +44,7 @@ app.register(FastifyOAuth2, { }, }, startRedirectPath: '/login', - scope: ['profile', 'email', 'phone', 'openid', 'address'], + scope: ['profile', 'email', 'openid'], callbackUri: 'http://localhost:4010/login/keycloak/callback', discovery: { issuer: 'http://localhost:8080/realms/master' }, }); @@ -61,26 +63,26 @@ app.addHook('onSend', async function (_, reply) { const engine = await startPlatform({ modules: baseModules, context: (contextResolver: UnchainedContextResolver) => async (props, req) => { + // eslint-disable-next-line const keycloakInstance = (app as any).keycloak as FastifyOAuth2.OAuth2Namespace; const context = await contextResolver(props); if (context.user || !req.session.keycloak) return context; try { const isExpired = new Date(req.session.keycloak.expires_at) < new Date(); if (isExpired) { - req.session.keycloak = await keycloakInstance.getNewAccessTokenUsingRefreshToken( - req.session.keycloak, - {}, - ); + req.session.keycloak = ( + await keycloakInstance.getNewAccessTokenUsingRefreshToken(req.session.keycloak, {}) + ).token; } - const userinfo = await keycloakInstance.userinfo(req.session.keycloak.access_token); - const { sub, resource_access, preferred_username } = userinfo as { + const decoded = jwt.decode(req.session.keycloak.id_token); + const { sub, groups, preferred_username } = decoded as { sub: string; - resource_access: Record; + groups: string[]; preferred_username: string; }; - const roles = resource_access?.['unchained-local']?.roles || []; + const roles = groups || []; const username = preferred_username || `keycloak:${sub}`; let user = await context.modules.users.findUserByUsername(username); if (roles.join(':') !== user.roles.join(':')) { @@ -91,8 +93,24 @@ const engine = await startPlatform({ ...context, userId: user._id, user, + logout: async () => { + const tokenObject = { + // eslint-disable-next-line + _id: (req as any).session.sessionId, + userId: user._id, + }; + try { + await keycloakInstance.revokeAllToken(req.session.keycloak, undefined); + } catch { + /* */ + } + req.session.keycloak = null; + await emit(API_EVENTS.API_LOGOUT, tokenObject); + return true; + }, }; } catch (e) { + console.error(e); delete req.session.keycloak; } return { @@ -114,28 +132,14 @@ app.get( ) { try { const accessToken = await this.keycloak.getAccessTokenFromAuthorizationCodeFlow(request); - const userinfo = await this.keycloak.userinfo(accessToken.token.access_token); - const { - sub, - email, - resource_access, - email_verified, - name, - given_name, - family_name, - preferred_username, - } = userinfo as { + const decoded = jwt.decode(accessToken.token.id_token); + const { sub, groups, preferred_username } = decoded as { sub: string; - email?: string; - resource_access: Record; - email_verified: boolean; - name?: string; - given_name?: string; - family_name?: string; + groups: string[]; preferred_username: string; }; - const roles = resource_access?.['unchained-local']?.roles || []; + const roles = groups || []; const username = preferred_username || `keycloak:${sub}`; const user = await request.unchainedContext.modules.users.findUserByUsername(username); @@ -144,14 +148,7 @@ app.get( { username, password: null, - email: email_verified ? email : undefined, - profile: { - displayName: name, - address: { - firstName: given_name, - lastName: family_name, - }, - }, + profile: {}, roles, }, { skipMessaging: true, skipPasswordEnrollment: true }, @@ -159,8 +156,8 @@ app.get( } // eslint-disable-next-line // @ts-ignore - request.session.keycloak = accessToken; - return reply.redirect('/'); + request.session.keycloak = accessToken.token; + return reply.redirect('http://localhost:3000/'); } catch (e) { console.error(e); reply.status(500); diff --git a/packages/api/src/api-index.ts b/packages/api/src/api-index.ts index eddb7b46f..513b1a346 100644 --- a/packages/api/src/api-index.ts +++ b/packages/api/src/api-index.ts @@ -7,7 +7,7 @@ import { UnchainedContextResolver, } from './context.js'; import { UnchainedCore } from '@unchainedshop/core'; - +export * from './events.js'; export * from './context.js'; export * from './locale-context.js'; export * from './loaders/index.js'; diff --git a/packages/api/src/context.ts b/packages/api/src/context.ts index 09da0cb52..2c4742bfa 100644 --- a/packages/api/src/context.ts +++ b/packages/api/src/context.ts @@ -3,13 +3,11 @@ import instantiateLoaders, { UnchainedLoaders } from './loaders/index.js'; import { getLocaleContext, UnchainedLocaleContext } from './locale-context.js'; import { UnchainedServerOptions } from './api-index.js'; import { User } from '@unchainedshop/core-users'; -import { IncomingMessage, OutgoingMessage } from 'node:http'; export type LoginFn = ( user: User, options?: { impersonator?: User; - maxAge?: number; }, ) => Promise<{ _id: string; tokenExpires: Date }>; diff --git a/packages/api/src/fastify/index.ts b/packages/api/src/fastify/index.ts index b9e10e0e5..52ad23952 100644 --- a/packages/api/src/fastify/index.ts +++ b/packages/api/src/fastify/index.ts @@ -42,18 +42,16 @@ const middlewareHook = async function middlewareHook(req: any, reply: any) { const context = getCurrentContextResolver(); const login: LoginFn = async function (user: User, options = {}) { - const { impersonator, maxAge } = options; + const { impersonator } = options; req.session.userId = user._id; req.session.impersonatorId = impersonator?._id; - req.session.loginExpires = maxAge - ? new Date(Date.now() + maxAge) /* eslint-disable-next-line */ - : new Date((req as any).session.cookie._expires); const tokenObject = { _id: req.session.sessionId, userId: user._id, - tokenExpires: req.session.loginExpires, + // eslint-disable-next-line + tokenExpires: new Date((req as any).session.cookie._expires), }; await emit(API_EVENTS.API_LOGIN_TOKEN_CREATED, tokenObject); /* eslint-disable-next-line */ @@ -63,7 +61,6 @@ const middlewareHook = async function middlewareHook(req: any, reply: any) { const logout: LogoutFn = async function logout() { /* eslint-disable-line */ - if (!req.session?.userId) return false; const tokenObject = { _id: (req as any).session.sessionId, userId: req.session?.userId,