Skip to content

Commit

Permalink
Fix logout and use jwt parsing instead of live call to userinfo
Browse files Browse the repository at this point in the history
  • Loading branch information
pozylon committed Jan 4, 2025
1 parent 50c5fbb commit d65c4fe
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 47 deletions.
73 changes: 35 additions & 38 deletions examples/keycloak/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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' },
});
Expand All @@ -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<string, { roles: string[] }>;
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(':')) {
Expand All @@ -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 {
Expand All @@ -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<string, { roles: string[] }>;
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);

Expand All @@ -144,23 +148,16 @@ 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 },
);
}
// 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);
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/api-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 0 additions & 2 deletions packages/api/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;

Expand Down
9 changes: 3 additions & 6 deletions packages/api/src/fastify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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,
Expand Down

0 comments on commit d65c4fe

Please sign in to comment.