# STEP 03 – Adapt GraphQL context, authorization, and docs to SwissOID sessions Aligned the GraphQL layer with the session-based flow exposed by `swissoid-back`, eliminating the old JWT token helpers and ensuring resolvers consume the shared session service. ## Updated types (`biblio-stats-graphql/src/generalTypes.ts`) ```ts import { ActionOutcomeError, ActionOutcomeFail, ActionOutcomeSuccess, ActionStatus, UUIDProp } from 'graphql-knifey'; import type { Request } from 'express'; import type { CookieManager, SwissOIDSessionService } from 'swissoid-back'; import UserService from './services/UserService'; import StatService from './services/StatService'; import type { AppConfig } from './config/appConfig'; export type BiblioStatsGraphQLContext = { statService: StatService; userService: UserService; coreToDomain: (core: string) => string; sessionService?: SwissOIDSessionService; cookieManager?: CookieManager; appConfig?: AppConfig; logger?: { log: (...args: any[]) => void }; token?: string; req?: Request & { cookies?: Record }; user?: { UUID?: string; email?: string; username?: string; sub?: string; }; }; ``` ## Session-backed authorization (`biblio-stats-graphql/src/utils/authorize.ts`) ```ts import type { SessionData } from 'swissoid-back'; import { BiblioStatsGraphQLContext, UserProp } from '../generalTypes'; import { getFailOutcomeFromError } from 'graphql-knifey'; import { ActionOutcomeFail, ActionOutcomeForbidden, ActionStatus, GqlResolversContextParams } from 'graphql-knifey/build/src/generalTypes'; const unauthenticatedOutcome: ActionOutcomeFail<'UNAUTHENTICATED'> = { status: ActionStatus.fail, code: 'UNAUTHENTICATED', message: 'Authentication required', }; function buildUserFromSession(session: SessionData) { return { UUID: session.sub, email: session.email, username: session.metadata?.username ?? session.name ?? session.email, sub: session.sub, }; } async function resolveSession(context: GqlResolversContextParams) { const { sessionService, cookieManager, req } = context; if (!sessionService || !req) { return null; } const cookieName = context.appConfig?.sessionCookieName || 'sid'; let sessionId = cookieManager?.getSessionId?.(req) || req.cookies?.[cookieName]; if (!sessionId) { const rawCookies = req.headers?.cookie ?? req.headers?.Cookie; const cookieHeader = Array.isArray(rawCookies) ? rawCookies.join(';') : rawCookies; if (cookieHeader) { sessionId = cookieHeader .split(';') .map((cookie) => cookie.trim().split('=') as [string, string]) .find(([name]) => name === cookieName)?.[1]; } } if (!sessionId) { return null; } const session = await sessionService.getSession(sessionId); return session ?? null; } export async function authorized( context: GqlResolversContextParams, input: T ): Promise<{ shouldReturn: true; value: ActionOutcomeForbidden | ActionOutcomeFail<'ERROR'> | ActionOutcomeFail<'UNAUTHENTICATED'> } | { shouldReturn: false; value: UserProp & T }> { try { const session = await resolveSession(context); if (!session) { return { shouldReturn: true, value: unauthenticatedOutcome }; } const props = { ...input, user: buildUserFromSession(session), }; context.user = props.user; const isAllowed = await context.userService.existsOrRegisterAndIfExistsIsAllowed({ user: props.user }); if (!isAllowed) { return { shouldReturn: true, value: { status: ActionStatus.fail, code: 'FORBIDDEN', message: "You don't have permission to perform this request", }, }; } return { shouldReturn: false, value: props, }; } catch (err) { return { shouldReturn: true, value: getFailOutcomeFromError(err as Error), }; } } ``` ## Resolver integration (`biblio-stats-graphql/src/graphql/resolvers/index.ts`) ```ts import { GQLResolverDict, getFailOutcomeFromError } from 'graphql-knifey'; // ... registerUser: async (_, __, context) => { try { const authResult = await authorized(context, {}); if (authResult.shouldReturn) return authResult.value; return await context.userService.registerUser(authResult.value); } catch (err) { return getFailOutcomeFromError(err as Error); } }, ``` ## Documentation (`biblio-stats-graphql/README.md`) ```md Authentication is handled by the shared SwissOID backend (`swissoid-back`). The service mounts the reusable OIDC routes so `/login`, `/oidc/callback`, `/auth/status`, and `/auth/logout` follow the exact same flow as cronide-user. Sessions are stored in Redis and materialised as HttpOnly cookies on the relying party domain. Resolvers read the active session via `sessionService` and `cookieManager` from `swissoid-back`. When a valid session cookie is present the user UUID/email is injected into the GraphQL context and used for downstream authorization. ``` Legacy JWT helpers were deleted (`src/loaders/tokenAuthService.ts`, `src/middleware/swissoidAuth.ts`, `src/setupAuth.ts`, and related middleware wrappers) because the SwissOID middleware now covers the full flow.