GraphQL is basically a query language that can be used to request data from an API. Currently, the most common and popular API pattern is REST. But as I see, the best improvement of GraphQL than the REST is that it can get everything from a single request that REST has to make few API calls to retrieve.
But at the same time, this can be the great weakness of GraphQL too. Because if a user decides to request so much data from a single request, the server can be overloaded. But most of the time, both the frontend and the backend are developed by the same set of developers. So I think that won’t be a problem because the developers can manage the requests. Error handling in a GraphQL response can be tricky a bit with GraphQL but it’s worth it because of its many advantages.
In the below, I have explained how to integrate GraphQL with authentication with a NestJS project.
Prerequisites
You will need the base created in my previous article to continue with this guide.
Integrating GraphQL
In this guide, I’m following the Code First method.
First of all, you need to install the following dependencies and dev-dependencies.
npm i @nestjs/graphql graphql-tools graphql apollo-server-express npm i --save-dev @apollo/gateway
First of all, open the app.module.ts
and import the GraphQLModule
as follows. At the same time, the ThrottlerGuard
that we have placed should be removed. We later will reapply the Throttler Guard.
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; import { ThrottlerModule } from '@nestjs/throttler'; import { bruteForceLimits } from './config/auth.config'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import { join } from 'path'; @Module({ imports: [ ConfigModule.forRoot({ envFilePath: `${process.env.NODE_ENV || 'development'}.env`, isGlobal: true }), AuthModule, UsersModule, ThrottlerModule.forRoot({ ttl: bruteForceLimits.ttl, limit: bruteForceLimits.requestLimit }), GraphQLModule.forRoot({ autoSchemaFile: join(process.cwd(), 'src/schema.gql'), debug: false, playground: false }) ], controllers: [ AppController ], providers: [ AppService ], }) export class AppModule {}
Now let’s create a type for users. Create a file named user.model.ts
in the users
directory and enter the following definitions.
import { Field, ID, ObjectType } from "@nestjs/graphql"; import { Permission } from "src/enums/permission.enum"; @ObjectType() export class User { @Field(type => ID) id: number @Field() username: string @Field(type => [String]) permissions: Array<Permission> }
Next, we need to create a resolver using the following command.
nest g r users
And implement its code as follows.
import { NotFoundException } from '@nestjs/common'; import { Args, Query, Resolver } from '@nestjs/graphql'; import { User } from './user.model'; import { UsersService } from './users.service'; @Resolver(of => User) export class UsersResolver { constructor(private readonly userService: UsersService) {} @Query(returns => User) async user(@Args('id') id: number): Promise<User> { const user = await this.userService.findByID(id) if(!user) { throw new NotFoundException(id) } const { password, ...result } = user return result } }
Then open the users.module.ts
and add UsersResolver in the providers and remove controllers since we’re going to replace the REST method.
import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersResolver } from './users.resolver'; @Module({ providers: [ UsersService, UsersResolver ], exports: [UsersService], }) export class UsersModule {}
Now the GraphQL integration is done. You should get results for the following query.
curl -X POST http://localhost:3000/graphql -d '{"query":"{user(id:1){id,username}}"}' -H "Content-Type: application/json"
Integrating JWT Authentication
When integrating JWT authentication, we need to create a guard overriding getRequest
method. Use the following command to create the guard.
nest g gu auth/guards/gql-auth
Then implement its code as below.
import { ExecutionContext, Injectable } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class GqlAuthGuard extends AuthGuard('jwt') { getRequest(context: ExecutionContext) { const ctx = GqlExecutionContext.create(context) return ctx.getContext().req } }
Then open the users.resolver.ts
file and add the UseGuard decorator to the entire class so all the resolvers will be guarded. As it was said in the previous guide, we’re not going to use the guard in the users.module.ts
file because it’s imported in the Auth Module as well.
import { NotFoundException, UseGuards } from '@nestjs/common'; import { Args, Query, Resolver } from '@nestjs/graphql'; import { GqlAuthGuard } from 'src/auth/guards/gql-auth.guard'; import { User } from './user.model'; import { UsersService } from './users.service'; @Resolver(of => User) @UseGuards(GqlAuthGuard) export class UsersResolver { constructor(private readonly userService: UsersService) {} @Query(returns => User) async user(@Args('id') id: number): Promise<User> { const user = await this.userService.findByID(id) if(!user) { throw new NotFoundException(id) } const { password, ...result } = user return result } }
Now if you try the previous curl, you will get an unauthorized error. You have to get the token as we did in the previous guide and send it as a bearer token in the request.
Brute-force Attacks Preventing
The throttler guard is removed at the beginning of this guide. Now let’s first reintroduce it to the Auth module so the authentication requests will be protected. Open the auth.controller.ts
file and use UseGuards
decorator to guard the entire class with the Throttler Guard.
import { Controller, Post, Request, UseGuards } from '@nestjs/common'; import { ThrottlerGuard } from '@nestjs/throttler'; import { AuthService } from './auth.service'; import { LocalAuthGuard } from './guards/local-auth.guard'; @Controller('auth') @UseGuards(ThrottlerGuard) export class AuthController { constructor(private authService: AuthService) {} @UseGuards(LocalAuthGuard) @Post('login') async login(@Request() req) { return this.authService.sign(req.user) } }
Now we have to introduce the Throttler Guard to the GraphQL resolvers. For that first, we need to create a guard overriding getRequestResponse
method as follows.
nest g gu security/guards/gql-throttler
And implement its code as follows.
import { ExecutionContext, Injectable } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; import { ThrottlerGuard } from "@nestjs/throttler"; @Injectable() export class GqlThrottlerGuard extends ThrottlerGuard { getRequestResponse(context: ExecutionContext) { const gqlCtx = GqlExecutionContext.create(context) const ctx = gqlCtx.getContext() return { req: ctx.req, res: ctx.req.res } } }
In the official documentation, return { req: ctx.req, res: ctx.req.res }
line is changed as return { req: ctx.req, res: ctx.res }
but it didn’t work for me. So you might want to use it as it’s in my guide.
Then open the users.resolver.ts
file and add the newly created guard to the guard list.
import { NotFoundException, UseGuards } from '@nestjs/common'; import { Args, Query, Resolver } from '@nestjs/graphql'; import { GqlAuthGuard } from 'src/auth/guards/gql-auth.guard'; import { GqlThrottlerGuard } from 'src/security/guards/gql-throttler.guard'; import { User } from './user.model'; import { UsersService } from './users.service'; @Resolver(of => User) @UseGuards(GqlThrottlerGuard, GqlAuthGuard) export class UsersResolver { constructor(private readonly userService: UsersService) {} @Query(returns => User) async user(@Args('id') id: number): Promise<User> { const user = await this.userService.findByID(id) if(!user) { throw new NotFoundException(id) } const { password, ...result } = user return result } }
Authorization
When creating authorization for the GraphQL resolver, we have to change the previously created permissions guard. So let’s create a new permission guard for GraphQL requests.
nest g gu auth/guards/gql-permission
Implement the code as follows. You can notice that it has small differences from the previously created permission guard.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { GqlExecutionContext } from '@nestjs/graphql'; import { PERMISSIONS_KEY } from 'src/decorators/permissions.decorator'; import { Permission } from 'src/enums/permission.enum'; @Injectable() export class GqlPermissionsGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const gqlCtx = GqlExecutionContext.create(context) const ctx = gqlCtx.getContext() const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(PERMISSIONS_KEY, [ context.getHandler(), context.getClass() ]) if(!requiredPermissions) { return true } const { user } = ctx.req return requiredPermissions.some((permission) => user.permissions?.includes(permission)) } }
Now we have to use the newly created guard in the resolver and add permission for each query as follows.
import { NotFoundException, UseGuards } from '@nestjs/common'; import { Args, Query, Resolver } from '@nestjs/graphql'; import { GqlAuthGuard } from 'src/auth/guards/gql-auth.guard'; import { GqlPermissionsGuard } from 'src/auth/guards/gql-permission.guard'; import { Permissions } from 'src/decorators/permissions.decorator'; import { Permission } from 'src/enums/permission.enum'; import { GqlThrottlerGuard } from 'src/security/guards/gql-throttler.guard'; import { User } from './user.model'; import { UsersService } from './users.service'; @Resolver(of => User) @UseGuards(GqlThrottlerGuard, GqlAuthGuard, GqlPermissionsGuard) export class UsersResolver { constructor(private readonly userService: UsersService) {} @Query(returns => User) @Permissions(Permission.GET_USER) async user(@Args('id') id: number): Promise<User> { const user = await this.userService.findByID(id) if(!user) { throw new NotFoundException(id) } const { password, ...result } = user return result } }
So that’s it
If you have any suggestions or questions about this guide, comment here or send me an email.