Guards & context
The GraphQL variant reuses the same SessionAuthGuard and CurrentUser decorator as REST, but they detect GraphQL context and read the request from GqlExecutionContext.
SessionAuthGuard in GraphQL
Section titled “SessionAuthGuard in GraphQL”async canActivate(context: ExecutionContext): Promise<boolean> { let request: SessionRequest;
if (context.getType<string>() === 'graphql') { const gqlCtx = GqlExecutionContext.create(context); request = gqlCtx.getContext().req; } else { request = context.switchToHttp().getRequest<SessionRequest>(); }
const userId = request.session?.userId; if (!userId) { throw new UnauthorizedException('Session not found'); } const user = await this.userService.findById(userId); if (!user) { throw new UnauthorizedException('User not found'); } request.user = user; return true;}This means the same guard works on both REST controllers and GraphQL resolvers without duplication.
CurrentUser decorator
Section titled “CurrentUser decorator”export const CurrentUser = createParamDecorator( (data: keyof SessionRequest['user'] | undefined, ctx: ExecutionContext) => { let request: SessionRequest;
if (ctx.getType<string>() === 'graphql') { const gqlCtx = GqlExecutionContext.create(ctx); request = gqlCtx.getContext().req; } else { request = ctx.switchToHttp().getRequest<SessionRequest>(); }
const { user } = request; if (!user) { throw new Error('User not found in request'); } if (data) { return user[data]; } return user; },);Use it the same way in both variants:
@UseGuards(SessionAuthGuard)@Query(() => UserType)async currentUser(@CurrentUser() user: User) { return this.userService.findById(user.id);}LocalGuard in GraphQL
Section titled “LocalGuard in GraphQL”The GraphQL variant reuses the same LocalGuard as REST. The only difference is that it maps the GraphQL loginInput argument onto request.body so Passport’s local strategy can read the credentials.
@Injectable()export class LocalGuard extends AuthGuard('local') { getRequest(context: ExecutionContext): any { const ctx = GqlExecutionContext.create(context); const request = (ctx.getContext<{ req?: any }>()?.req || context.switchToHttp().getRequest<any>()) as { body?: any };
// GraphQL passes credentials as args; passport-local expects them on body. if (ctx.getContext<{ req?: any }>()?.req) { const args = ctx.getArgs<{ loginInput?: { email?: string; password?: string } }>(); if (args.loginInput) { request.body = { email: args.loginInput.email, password: args.loginInput.password, }; } }
return request; }}Because request.body is populated from args.loginInput, the login resolver can use the same @UseGuards(LocalGuard) and @CurrentUser() pattern as the REST controller:
@UseGuards(LocalGuard)@Mutation(() => AuthResponseType)async login( @Context() ctx: { req: SessionRequest }, @CurrentUser() user: User,): Promise<AuthResponseType> { return this.authenticationService.login(ctx.req, user);}The guard validates the user and attaches it to the request, exactly like the REST endpoint.
Context shape
Section titled “Context shape”The GraphQL context contains:
req— The Express request, including session.res— The Express response.loaders— Per-request DataLoaders.
Access it with @Context():
@Mutation(() => MessageResponseType)async logout(@Context() ctx: { req: SessionRequest }) { return this.authenticationService.logout(ctx.req);}CSRF with GraphQL
Section titled “CSRF with GraphQL”GraphQL mutations are POST requests, so they are covered by the CSRF middleware. The client must include the CSRF token header and credentials. cookie-parser is installed and registered in main.ts, so the CSRF middleware works for both REST POST endpoints and GraphQL mutations.