DEV Community

Cover image for NestJS Authentication with OAuth2.0: Apollo Local OAuth GraphQL API
Afonso Barracha
Afonso Barracha

Posted on

NestJS Authentication with OAuth2.0: Apollo Local OAuth GraphQL API

Hey there, dear readers! I apologise for the delayed Article. I had been traveling abroad for almost a month, and upon returning, this series slipped my conscious. I am sorry for any frustration that this may have caused.

We are nearing the end of this series, with only two more articles to go (including this one). I am committed to delivering them before mid-April, so you will not have to wait too long. But hold on, the excitement does not end there! I have got big plans for the future of my blog.

In fact, I am thrilled to announce that I'll be introducing two new series:

  • Rust Algorithm Development: In this series, I will be guiding you through implementing some basic NLP algorithms from scratch. Specifically, TF-IDF, Co-occurrence Matrix and RAKE. I know it might seem a bit random, but since machine learning is becoming more prevalent in our lives, I thought it would be fun to dip my toes into the topic since I used to be a Data Analyst (technically an Econometrician)
  • Advance GraphQL with Mercurius and NestJS: Throughout this series, I will be your guide as we develop a Discord clone. We will start by creating a monolithic back-end and then divide it into microservices to make it more scalable and future proof.

I hope you all enjoy the remainder of the series, and thank you for your understanding!

Series Intro

This series will cover the full implementation of OAuth2.0 Authentication in NestJS for the following types of APIs:

And it is divided in 5 parts:

  • Configuration and operations;
  • Express Local OAuth REST API;
  • Fastify Local OAuth REST API;
  • Apollo Local OAuth GraphQL API;
  • Adding External OAuth Providers to our API;

Lets start the fourth part of this series.

Tutorial Intro

On this tutorial we will look at the previous Express REST API tutorial, and transform it to an Apollo GraphQL API.

DISCLAIMER: Although it is possible to have an OAuth System in GraphQL, it is recommended to use REST architecture instead. This article is for educational purposes only. Please do not use this code in Production. To discourage the use of this code in production I removed all unit tests from it

NOTE: if you do not have time to read the entire article the repository can be found here. Do not use this code in production, and please remember that there is nothing wrong with having a Hybrid API, with REST for Authentication and GraphQL for the rest of the API, that is what is recommended. While you at it consider buying me a coffee.

Set up

Disclaimers aside, start by installing the required libraries for GraphQL development:

$ yarn add @nestjs/graphql @nestjs/apollo @apollo/server graphql @apollo/federation @apollo/subgraph
Enter fullscreen mode Exit fullscreen mode

On the config directory create a new file for the GraphQL configuration:

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { ConfigService } from '@nestjs/config';
import { GqlOptionsFactory } from '@nestjs/graphql';
import { IContext } from './interfaces/context.interface';

export class GraphQLConfig implements GqlOptionsFactory {
  private readonly testing: boolean;

  constructor(private readonly configService: ConfigService) {
    this.testing = this.configService.get('testing');
  }

  public createGqlOptions(): ApolloDriverConfig {
    return {
      driver: ApolloDriver,
      context: ({ req, res }): IContext => ({
        req,
        res,
      }),
      path: '/api/graphql',
      autoSchemaFile: './schema.gql',
      sortSchema: true,
      playground: this.testing,
      introspection: true,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

And register the GraphQLModule with it in the AppModule:

// ...
import { GraphQLConfig } from './config/graphql.config';
// ...

@Module({
  imports: [
    // ...
    GraphQLModule.forRootAsync({
      imports: [ConfigModule],
      driver: ApolloDriver,
      useClass: GraphQLConfig,
    }),
    // ...
  ],
  // ...
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Differences overview

If you are reading this article, I will assume that you already have experience developing GraphQL APIs using NestJS, but to summarise there are a few differences from REST to GraphQL:

  • We use Resolvers and not Controllers to write Mutations (equivalent to POST, PATCH & Delete) and Queries (GET equivalent);
  • Request and Response are not passed directly to the Resolver, but through the Context parameter.

Having this in mind, going from REST to GraphQL is not that hard.

Resolvers

In general before developing our resolvers we need to create:

  • Object Types:
    • How our entities maps to the GraphQL schema;
    • Impl: add @ObjectType() decorator on the entity.
  • Args Types:
    • The arguments that go into our Queries (like array fields in Python *args);
    • Impl: add @ArgsType() decorator on DTOs.
  • Input Types:
    • This are Input Objects where a single argument will accept an object (like named parameters in Python **kwargs);
    • Impl: add @InputType() decorator on DTOs.
  • Fields:
    • For any field that should be mapped on any kind of object or DTO;
    • Impl: add @Field decorator on each parameter.

As for standards and rules we will use the Apollo recommendations and be explicit with our queries and mutations.

Updates

Common

On the common service start by creating a new entities folder with a gql folder inside and add a MessageType there, based on the MessageMapper:

message.type.ts:

import { Field, ObjectType } from '@nestjs/graphql';
import { ApiProperty } from '@nestjs/swagger';
import { v4 } from 'uuid';
import { IMessage } from '../../interfaces/message.interface';

@ObjectType('Message')
export class MessageType implements IMessage {
  @Field(() => String)
  public id: string;

  @Field(() => String)
  @ApiProperty({
    description: 'Message',
    example: 'Hello World',
    type: String,
  })
  public message: string;

  constructor(message: string) {
    this.id = v4();
    this.message = message;
  }
}
Enter fullscreen mode Exit fullscreen mode

We need a new DTO as well, the IdDto, start by creating a dtos folder and add the id.dto.ts there:

import { ArgsType, Field, Int } from '@nestjs/graphql';
import { IsInt, Min } from 'class-validator';

@ArgsType()
export abstract class IdDto {
  @Field(() => Int)
  @IsInt()
  @Min(1)
  public id: number;
}
Enter fullscreen mode Exit fullscreen mode

Note that if your service should allow to filter users consider reading my article on cursor pagination. Shameless plug I know

Lastly since we are creating a micro-service we need to Federate our schema, so create a IFederatedInstance interface on the interfaces folder:

export interface IFederatedInstance<T extends string> {
  readonly __typename: T;
  readonly id: number;
}
Enter fullscreen mode Exit fullscreen mode

Read more about apollo federation on the apollo docs.

Users

Entities

Start by adding the @ObjectType decorator to the UserEntity, with the @Field decorator for every field you want to be queryable:

import { Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
import { Directive, Field, Int, ObjectType } from '@nestjs/graphql';
import { IsBoolean, IsEmail, IsString, Length, Matches } from 'class-validator';
import {
  BCRYPT_HASH,
  NAME_REGEX,
  SLUG_REGEX,
} from '../../common/consts/regex.const';
import { CredentialsEmbeddable } from '../embeddables/credentials.embeddable';
import { IUser } from '../interfaces/user.interface';
import { privateMiddleware } from '../middleware/private.middleware';

@ObjectType('User')
@Directive('@key(fields: "id")')
@Entity({ tableName: 'users' })
export class UserEntity implements IUser {
  @Field(() => Int)
  @PrimaryKey()
  public id: number;

  @Field(() => String)
  @Property({ columnType: 'varchar', length: 100 })
  @IsString()
  @Length(3, 100)
  @Matches(NAME_REGEX, {
    message: 'Name must not have special characters',
  })
  public name: string;

  @Field(() => String)
  @Property({ columnType: 'varchar', length: 106 })
  @IsString()
  @Length(3, 106)
  @Matches(SLUG_REGEX, {
    message: 'Username must be a valid slug',
  })
  public username: string;

  @Field(() => String, { nullable: true, middleware: [privateMiddleware] })
  @Property({ columnType: 'varchar', length: 255 })
  @IsString()
  @IsEmail()
  @Length(5, 255)
  public email: string;

  @Property({ columnType: 'varchar', length: 60 })
  @IsString()
  @Length(59, 60)
  @Matches(BCRYPT_HASH)
  public password: string;

  @Property({ columnType: 'boolean', default: false })
  @IsBoolean()
  public confirmed: true | false = false;

  @Embedded(() => CredentialsEmbeddable)
  public credentials: CredentialsEmbeddable = new CredentialsEmbeddable();

  @Property({ onCreate: () => new Date() })
  public createdAt: Date = new Date();

  @Property({ onUpdate: () => new Date() })
  public updatedAt: Date = new Date();
}
Enter fullscreen mode Exit fullscreen mode

In order for only the current logged user to have access to his own email we need to create a privateMiddleware, add it to new a middleware directory:

import { FieldMiddleware, MiddlewareContext } from '@nestjs/graphql';
import { isNull, isUndefined } from '../../common/utils/validation.util';
import { IContext } from '../../config/interfaces/context.interface';
import { IUser } from '../interfaces/user.interface';

export const privateMiddleware: FieldMiddleware = async (
  ctx: MiddlewareContext<IUser, IContext, unknown>,
  next,
) => {
  const user = ctx.context.req.user;

  if (isUndefined(user) || isNull(user) || ctx.source.id !== user) {
    return null;
  }

  return next();
};
Enter fullscreen mode Exit fullscreen mode

Users Service

Before defining the new users.resolver.ts file we need to do some changes to UsersService based on being explicit with our mutation, hence we need to divide the update method into each user's parameter:

  • updateName:

    @Injectable()
    export class UsersService {
      // ...
    
      public async updateName(userId: number, name: string): Promise<UserEntity> {
        const user = await this.findOneById(userId);
        const formattedName = this.commonService.formatName(name);
    
        if (user.name === formattedName) {
          throw new BadRequestException('Name must be different');
        }
    
        user.name = formattedName;
        await this.commonService.saveEntity(this.usersRepository, user);
        return user;
      }
    
      // ...
    }
    
  • updateUsername:

    @Injectable()
    export class UsersService {
      // ...
    
      public async updateUsername(
        userId: number,
        username: string,
      ): Promise<UserEntity> {
        const user = await this.findOneById(userId);
        const formattedUsername = username.toLowerCase();
    
        if (user.username === formattedUsername) {
          throw new BadRequestException('Username should be different');
        }
    
        await this.checkUsernameUniqueness(formattedUsername);
        user.username = formattedUsername;
        await this.commonService.saveEntity(this.usersRepository, user);
        return user;
      }
    
      // ...
    }
    

DTOs

On the DTOs we created in the previous tutorials, and new DTOs you will need to add the @ArgsType() decorator. So the old decorators would be something like this:

  • PasswordDto on password.dto.ts:

    import { ArgsType, Field } from '@nestjs/graphql';
    import { ApiProperty } from '@nestjs/swagger';
    import { IsString, MinLength } from 'class-validator';
    
    @ArgsType()
    export abstract class PasswordDto {
      @Field(() => String)
      @ApiProperty({
        description: 'The password of the user',
        minLength: 1,
        type: String,
      })
      @IsString()
      @MinLength(1)
      public password: string;
    }
    
  • ChangeEmailDto on change-email.dto.ts;

    import { ArgsType, Field } from '@nestjs/graphql';
    import { ApiProperty } from '@nestjs/swagger';
    import { IsEmail, IsString, Length } from 'class-validator';
    import { PasswordDto } from './password.dto';
    
    @ArgsType()
    export abstract class ChangeEmailDto extends PasswordDto {
      @Field(() => String)
      @ApiProperty({
        description: 'The email of the user',
        example: 'someone@gmail.com',
        minLength: 5,
        maxLength: 255,
        type: String,
      })
      @IsString()
      @IsEmail()
      @Length(5, 255)
      public email: string;
    }
    

As for new create a NameDto and UsernameDto:

  • name.dto.ts:

    import { ArgsType, Field } from '@nestjs/graphql';
    import { IsString, Length, Matches } from 'class-validator';
    import { NAME_REGEX } from '../../common/consts/regex.const';
    
    @ArgsType()
    export abstract class NameDto {
      @Field(() => String)
      @IsString()
      @Length(3, 100)
      @Matches(NAME_REGEX, {
        message: 'Name must not have special characters',
      })
      public name: string;
    }
    
  • username.dto.ts:

    import { ArgsType, Field } from '@nestjs/graphql';
    import { IsString, Length, Matches } from 'class-validator';
    import { SLUG_REGEX } from '../../common/consts/regex.const';
    
    @ArgsType()
    export abstract class UsernameDto {
      @Field(() => String)
      @IsString()
      @Length(3, 106)
      @Matches(SLUG_REGEX, {
        message: 'Username must be a valid slug',
      })
      public username: string;
    }
    

Resolver

To create the users.resolver.ts run the following command:

$ nest g r users
Enter fullscreen mode Exit fullscreen mode

Now add a return type for our resolver and import the user service:

import { Resolver } from '@nestjs/graphql';
import { UserEntity } from './entities/user.entity';
import { UsersService } from './users.service';

@Resolver(() => UserEntity)
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}
}
Enter fullscreen mode Exit fullscreen mode

On the resolver we need to add two queries and four mutations.

Queries:

  • User by ID:

    import {
      Args,
      Query,
      // ...
    } from '@nestjs/graphql';
    import { Public } from '../auth/decorators/public.decorator';
    import { UserEntity } from './entities/user.entity';
    // ...
    
    @Resolver(() => UserEntity)
    export class UsersResolver {
      // ...
    
      @Public()
      @Query(() => UserEntity)
      public async userById(@Args() idDto: IdDto): Promise<UserEntity> {
        return await this.usersService.findOneById(idDto.id);
      }
    
    }
    
  • User by username:

    import {
      Args,
      Query,
      // ...
    } from '@nestjs/graphql';
    import { Public } from '../auth/decorators/public.decorator';
    import { UserEntity } from './entities/user.entity';
    // ...
    
    @Resolver(() => UserEntity)
    export class UsersResolver {
      // ...
    
      @Public()
      @Query(() => UserEntity)
      public async userByUsername(
        @Args() usernameDto: UsernameDto,
      ): Promise<UserEntity> {
        return await this.usersService.findOneByUsername(usernameDto.username);
      }
    
    }
    

Mutations

For mutations is a bit more complicated, specially because of delete. First we need to change the cookiePath to the GraphQL path '/api/graphl' so the cookie is blacklisted when the user deletes his or her account.

import { ConfigService } from '@nestjs/config';
// ...

@Resolver(() => UserEntity)
export class UsersResolver {
  private readonly cookiePath = '/api/graphql';
  private readonly cookieName: string;

  constructor(
    private readonly usersService: UsersService,
    private readonly configService: ConfigService,
  ) {
    this.cookieName = this.configService.get<string>('REFRESH_COOKIE');
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Finally, start adding the delete mutation:

// ...
import {
  // ...
  Context,
  Mutation,
  // ...
} from '@nestjs/graphql';
import { MessageType } from '../common/entities/gql/message.type';
import { PasswordDto } from './dtos/password.dto';
// ...

@Resolver(() => UserEntity)
export class UsersResolver {
  // ...

  @Mutation(() => MessageType)
  public async deleteUser(
    @Context('res') res: Response,
    @CurrentUser() id: number,
    @Args() passwordDto: PasswordDto,
  ): Promise<MessageType> {
    await this.usersService.delete(id, passwordDto);
    res.clearCookie(this.cookieName, { path: this.cookiePath });
    return new MessageType('User deleted successfully');
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see we use the context to pass the Response object into our mutation.

Next, create an update mutation for each editable parameter:

  • Update Email:

    // ...
    import { ChangeEmailDto } from './dtos/change-email.dto';
    // ...
    
    @Resolver(() => UserEntity)
    export class UsersResolver {
      // ...
    
      @Mutation(() => UserEntity)
      public async updateUserEmail(
        @CurrentUser() id: number,
        @Args() changeEmailDto: ChangeEmailDto,
      ): Promise<UserEntity> {
        return await this.usersService.updateEmail(id, changeEmailDto);
      }
    }
    
  • Update Name:

    // ...
    import { NameDto } from './dtos/name.dto';
    // ...
    
    @Resolver(() => UserEntity)
    export class UsersResolver {
      // ...
    
      @Mutation(() => UserEntity)
      public async updateUserName(
        @CurrentUser() id: number,
        @Args() nameDto: NameDto,
      ): Promise<UserEntity> {
        return await this.usersService.updateName(id, nameDto.name);
      }
    }
    
  • Update Username:

    // ...
    import { UsernameDto } from './dtos/username.dto';
    // ...
    
    @Resolver(() => UserEntity)
    export class UsersResolver {
      // ...
    
      @Mutation(() => UserEntity)
      public async updateUserUsername(
        @CurrentUser() id: number,
        @Args() usernameDto: UsernameDto,
      ): Promise<UserEntity> {
        return await this.usersService.updateUsername(id, usernameDto.username);
      }
    }
    

Finally since we are using micro-services, we need to add a @ResolveReference decorator so other sub-graphs can have access to the UserEntity:

// ...
import {
  // ...
  ResolveReference,
} from '@nestjs/graphql';
import { IFederatedInstance } from '../common/interfaces/federated-instance.interface';

@Resolver(() => UserEntity)
export class UsersResolver {
  // ...

  @ResolveReference()
  public async resolveReference(
    reference: IFederatedInstance<'User'>,
  ): Promise<UserEntity> {
    return this.usersService.findOneById(reference.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Auth

Entities

Create a gql directory and add an auth.type.ts type:

import { Field, ObjectType } from '@nestjs/graphql';
import { UserEntity } from '../../../users/entities/user.entity';
import { IUser } from '../../../users/interfaces/user.interface';

@ObjectType('Auth')
export abstract class AuthType {
  @Field(() => UserEntity)
  public user: IUser;

  @Field(() => String)
  public accessToken: string;
}
Enter fullscreen mode Exit fullscreen mode

DTOs

Start by updating the:

  • Email DTO:

    import { ArgsType, Field } from '@nestjs/graphql';
    import { ApiProperty } from '@nestjs/swagger';
    import { IsEmail, IsString, Length } from 'class-validator';
    
    @ArgsType()
    export abstract class EmailDto {
      @Field(() => String)
      @ApiProperty({
        description: 'The email of the user',
        example: 'someone@gmail.com',
        minLength: 5,
        maxLength: 255,
        type: String,
      })
      @IsString()
      @IsEmail()
      @Length(5, 255)
      public email: string;
    }
    
  • Passwords DTO:

    import { ArgsType, Field, InputType } from '@nestjs/graphql';
    import { ApiProperty } from '@nestjs/swagger';
    import { IsString, Length, Matches, MinLength } from 'class-validator';
    import { PASSWORD_REGEX } from '../../common/consts/regex.const';
    
    @InputType({ isAbstract: true })
    @ArgsType()
    export abstract class PasswordsDto {
      @Field(() => String)
      @ApiProperty({
        description: 'New password',
        minLength: 8,
        maxLength: 35,
        type: String,
      })
      @IsString()
      @Length(8, 35)
      @Matches(PASSWORD_REGEX, {
        message:
          'Password requires a lowercase letter, an uppercase letter, and a number or symbol',
      })
      public password1!: string;
    
      @Field(() => String)
      @ApiProperty({
        description: 'Password confirmation',
        minLength: 1,
        type: String,
      })
      @IsString()
      @MinLength(1)
      public password2!: string;
    }
    

Since we are going to use the AuthResolver to create new users and do updates that require complex inputs (e.g. updating the Password). We need to create an inputs folder and add the following inputs:

  • Sign In Input:

    import { Field, InputType } from '@nestjs/graphql';
    import { IsString, Length, MinLength } from 'class-validator';
    
    @InputType('SignInInput')
    export abstract class SignInInput {
      @Field(() => String)
      @IsString()
      @Length(3, 255)
      public emailOrUsername!: string;
    
      @Field(() => String)
      @IsString()
      @MinLength(1)
      public password!: string;
    }
    
  • Sign Up Input:

    import { Field, InputType } from '@nestjs/graphql';
    import { IsEmail, IsString, Length, Matches } from 'class-validator';
    import { NAME_REGEX } from '../../common/consts/regex.const';
    import { PasswordsDto } from '../dtos/passwords.dto';
    
    @InputType('SignUpInput')
    export abstract class SignUpInput extends PasswordsDto {
      @Field(() => String)
      @IsString()
      @Length(3, 100, {
        message: 'Name has to be between 3 and 100 characters.',
      })
      @Matches(NAME_REGEX, {
        message: 'Name can not contain special characters.',
      })
      public name!: string;
    
      @Field(() => String)
      @IsString()
      @IsEmail()
      @Length(5, 255)
      public email!: string;
    }
    
  • Reset Password Input:

    import { Field, InputType } from '@nestjs/graphql';
    import { IsJWT, IsString } from 'class-validator';
    import { PasswordsDto } from '../dtos/passwords.dto';
    
    @InputType('ResetPasswordInput')
    export abstract class ResetPasswordInput extends PasswordsDto {
      @Field(() => String)
      @IsString()
      @IsJWT()
      public resetToken!: string;
    }
    
  • Update Password Input:

    import { Field, InputType } from '@nestjs/graphql';
    import { IsString, MinLength } from 'class-validator';
    import { PasswordsDto } from '../dtos/passwords.dto';
    
    @InputType('UpdatePasswordInput')
    export abstract class UpdatePasswordInput extends PasswordsDto {
      @Field(() => String)
      @IsString()
      @MinLength(1)
      public password!: string;
    }
    

Decorators

The decorators need to be updated to accept GraphQL requests:

  • Current User Decorator:

    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    import { GqlExecutionContext } from '@nestjs/graphql';
    import { Request } from 'express-serve-static-core';
    import { IContext } from '../../config/interfaces/context.interface';
    
    export const CurrentUser = createParamDecorator(
      (_, context: ExecutionContext): number | undefined => {
        if (context.getType() === 'http') {
          return context.switchToHttp().getRequest<Request>()?.user;
        }
    
        return GqlExecutionContext.create(context).getContext<IContext>().req?.user;
      },
    );
    
  • Origin Decorator:

    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    import { GqlExecutionContext } from '@nestjs/graphql';
    import { Request } from 'express-serve-static-core';
    import { IContext } from '../../config/interfaces/context.interface';
    
    export const Origin = createParamDecorator(
      (_, context: ExecutionContext): string | undefined => {
        if (context.getType() === 'http') {
          return context.switchToHttp().getRequest<Request>().headers?.origin;
        }
    
        return GqlExecutionContext.create(context).getContext<IContext>().req
      .headers?.origin;
      },
    );
    

You can determine if you are using GraphQL or HTTP simply by using the context.getType() method.

Guards

The Auth Guard right now only accepts REST requests, so lets change that:

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { isJWT } from 'class-validator';
import { Request } from 'express-serve-static-core';
import { isNull, isUndefined } from '../../common/utils/validation.util';
import { TokenTypeEnum } from '../../jwt/enums/token-type.enum';
import { JwtService } from '../../jwt/jwt.service';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly jwtService: JwtService,
  ) {}

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    const activate = await this.setHttpHeader(
      context.switchToHttp().getRequest(),
      isPublic,
    );

    if (!activate) {
      throw new UnauthorizedException();
    }

    return activate;
  }

  /**
   * Sets HTTP Header
   *
   * Checks if the header has a valid Bearer token, validates it and sets the User ID as the user.
   */
  private async setHttpHeader(
    req: Request,
    isPublic: boolean,
  ): Promise<boolean> {
    const auth = req.headers?.authorization;

    if (isUndefined(auth) || isNull(auth) || auth.length === 0) {
      return isPublic;
    }

    const authArr = auth.split(' ');
    const bearer = authArr[0];
    const token = authArr[1];

    if (isUndefined(bearer) || isNull(bearer) || bearer !== 'Bearer') {
      return isPublic;
    }
    if (isUndefined(token) || isNull(token) || !isJWT(token)) {
      return isPublic;
    }

    try {
      const { id } = await this.jwtService.verifyToken(
        token,
        TokenTypeEnum.ACCESS,
      );
      req.user = id;
      return true;
    } catch (_) {
      return isPublic;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Resolver

The AuthResolver and AuthController are not that different, they do exactly the same thing, just change the routes to Queries and Mutations.

Start by creating the resolver:

$ nest g r auth
Enter fullscreen mode Exit fullscreen mode

And copy the AuthController constructor to the resolver:

import { ConfigService } from '@nestjs/config';
import { Resolver } from '@nestjs/graphql';
import { UsersService } from '../users/users.service';
import { AuthService } from './auth.service';

@Resolver(() => AuthType)
export class AuthResolver {
  private readonly cookiePath = '/api/graphql';
  private readonly cookieName: string;
  private readonly refreshTime: number;
  private readonly testing: boolean;

  constructor(
    private readonly authService: AuthService,
    private readonly usersService: UsersService,
    private readonly configService: ConfigService,
  ) {
    this.cookieName = this.configService.get<string>('REFRESH_COOKIE');
    this.refreshTime = this.configService.get<number>('jwt.refresh.time');
    this.testing = this.configService.get<boolean>('testing');
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see now our cookie path is "/api/graphql", so create a new saveRefreshCookie:

// ...
import { Response } from 'express-serve-static-core';

@Resolver(() => AuthType)
export class AuthResolver {
  // ...

  private saveRefreshCookie(res: Response, refreshToken: string): void {
    res.cookie(this.cookieName, refreshToken, {
      secure: !this.testing,
      httpOnly: true,
      signed: true,
      path: this.cookiePath,
      expires: new Date(Date.now() + this.refreshTime * 1000),
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

And copy the private refreshTokenFromReq method as well:

import { UnauthorizedException } from '@nestjs/common';
// ...
import { Request, Response } from 'express-serve-static-core';

@Resolver(() => AuthType)
export class AuthResolver {
  // ...

  private refreshTokenFromReq(req: Request): string {
    const token: string | undefined = req.signedCookies[this.cookieName];

    if (isUndefined(token)) {
      throw new UnauthorizedException();
    }

    return token;
  }
}
Enter fullscreen mode Exit fullscreen mode

Taking in mind that all the routes are the same but now we use Mutations and Queries, hence we get the following mutations:

import { UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { Request, Response } from 'express-serve-static-core';
import { MessageType } from '../common/entities/gql/message.type';
import { IMessage } from '../common/interfaces/message.interface';
import { isUndefined } from '../common/utils/validation.util';
import { UsersService } from '../users/users.service';
import { AuthService } from './auth.service';
import { CurrentUser } from './decorators/current-user.decorator';
import { Origin } from './decorators/origin.decorator';
import { Public } from './decorators/public.decorator';
import { ConfirmEmailDto } from './dtos/confirm-email.dto';
import { EmailDto } from './dtos/email.dto';
import { AuthType } from './entities/gql/auth.type';
import { ResetPasswordInput } from './inputs/reset-password.input';
import { SignInInput } from './inputs/sign-in.input';
import { SignUpInput } from './inputs/sign-up.input';
import { UpdatePasswordInput } from './inputs/update-password.input';

@Resolver(() => AuthType)
export class AuthResolver {
  // ...

  @Public()
  @Mutation(() => MessageType)
  public async signUp(
    @Origin() origin: string | undefined,
    @Args('input') signUpInput: SignUpInput,
  ): Promise<IMessage> {
    return await this.authService.signUp(signUpInput, origin);
  }

  @Public()
  @Mutation(() => AuthType)
  public async signIn(
    @Context('res') res: Response,
    @Origin() origin: string | undefined,
    @Args('input') signInInput: SignInInput,
  ): Promise<AuthType> {
    const { refreshToken, ...authType } = await this.authService.signIn(
      signInInput,
      origin,
    );
    this.saveRefreshCookie(res, refreshToken);
    return authType;
  }

  @Public()
  @Mutation(() => AuthType)
  public async refreshAccess(
    @Context('req') req: Request,
    @Context('res') res: Response,
  ): Promise<AuthType> {
    const token = this.refreshTokenFromReq(req);
    const { refreshToken, ...authType } =
      await this.authService.refreshTokenAccess(token, req.headers.origin);
    this.saveRefreshCookie(res, refreshToken);
    return authType;
  }

  @Mutation(() => MessageType)
  public async logout(
    @Context('req') req: Request,
    @Context('res') res: Response,
  ): Promise<IMessage> {
    const token = this.refreshTokenFromReq(req);
    res.clearCookie(this.cookieName);
    return this.authService.logout(token);
  }

  @Public()
  @Mutation(() => AuthType)
  public async confirmEmail(
    @Origin() origin: string | undefined,
    @Args() confirmEmailDto: ConfirmEmailDto,
    @Context('res') res: Response,
  ) {
    const { refreshToken, ...authType } = await this.authService.confirmEmail(
      confirmEmailDto,
      origin,
    );
    this.saveRefreshCookie(res, refreshToken);
    return authType;
  }

  @Public()
  @Mutation(() => MessageType)
  public async forgotPassword(
    @Origin() origin: string | undefined,
    @Args() emailDto: EmailDto,
  ): Promise<IMessage> {
    return await this.authService.resetPasswordEmail(emailDto, origin);
  }

  @Public()
  @Mutation(() => MessageType)
  public async resetPassword(
    @Args('input') input: ResetPasswordInput,
  ): Promise<IMessage> {
    return await this.authService.resetPassword(input);
  }

  @Mutation(() => AuthType)
  public async updatePassword(
    @CurrentUser() id: number,
    @Origin() origin: string | undefined,
    @Args('input') input: UpdatePasswordInput,
    @Context('res') res: Response,
  ): Promise<AuthType> {
    const { refreshToken, ...authType } = await this.authService.updatePassword(
      id,
      input,
      origin,
    );
    this.saveRefreshCookie(res, refreshToken);
    return authType;
  }

  //...
}
Enter fullscreen mode Exit fullscreen mode

And the following me query:

// ...
import { Query } from '@nestjs/graphql';
// ...
import { UserEntity } from '../users/entities/user.entity';
// ...

@Resolver(() => AuthType)
export class AuthResolver {
  // ...

  @Query(() => UserEntity)
  public async me(@CurrentUser() id: number): Promise<UserEntity> {
    return await this.usersService.findOneById(id);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

With this you are now able to implement (although again, you should not) a Local OAuth API in GraphQL.

The repository for the code can be found here.

About the Author

Hey there! I am Afonso Barracha, your go-to econometrician who found his way into the world of back-end development with a soft spot for GraphQL. If you enjoyed reading this article, why not show some love by buying me a coffee?

Lately, I have been diving deep into more advanced subjects. As a result, I have switched from sharing my thoughts every week to posting once or twice a month. This way, I can make sure to bring you the highest quality content possible.

Do not miss out on any of my latest articles – follow me here on dev and LinkedIn to stay updated. I would be thrilled to welcome you to our ever-growing community! See you around!

Top comments (0)