Skip to content

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.

src/core/authentication/guards/session.guard.ts (GraphQL variant)
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.

src/core/authentication/decorators/current-user.decorator.ts (GraphQL variant)
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);
}

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.

src/core/authentication/guards/local.guard.ts (GraphQL variant)
@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:

src/core/authentication/authentication.resolver.ts
@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.

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);
}

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.