You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5.2 KiB
5.2 KiB
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)
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<string, string> };
user?: {
UUID?: string;
email?: string;
username?: string;
sub?: string;
};
};
Session-backed authorization (biblio-stats-graphql/src/utils/authorize.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<BiblioStatsGraphQLContext>) {
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<T extends {}>(
context: GqlResolversContextParams<BiblioStatsGraphQLContext>,
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)
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)
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.