NestJS Authentication with OAuth2.0: Apollo Local OAuth GraphQL API
Afonso Barracha
Posted on March 26, 2023
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:
- Express REST API;
- Fastify REST API;
- Apollo GraphQL API.
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
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,
};
}
}
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 {}
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.
- The arguments that go into our Queries (like array fields in Python
-
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.
- This are Input Objects where a single argument will accept an object (like named parameters in Python
-
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;
}
}
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;
}
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;
}
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();
}
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();
};
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
onpassword.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
onchange-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
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) {}
}
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');
}
// ...
}
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');
}
}
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);
}
}
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;
}
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;
}
}
}
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
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');
}
}
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),
});
}
}
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;
}
}
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;
}
//...
}
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);
}
// ...
}
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!
Posted on March 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.