JWT
Server
Nest

Use the CLI (not yet supported)

npx ryo-auth@latest add jwt-nestjs-prisma

Manual Installation

    • app.module.ts
    • main.ts
  • Install dependencies

    npm i @nestjs/config @prisma/client argon2 class-transformer class-validator cookie-parser

    Install dev dependencies

    npm i --dev prisma @types/cookie-parser @types/jsonwebtoken

    Setup your Prisma schema

    prisma/schema.prisma
    generator client {
      provider = "prisma-client-js"
    }
     
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
     
    model User {
      id        String   @id @default(uuid())
      email     String   @unique
      password  String
      name      String
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }

    App bootstrap

    main.ts
    import { NestFactory } from "@nestjs/core";
    import { AppModule } from "./app.module";
    import { ValidationPipe } from "@nestjs/common";
    import * as cookieParser from "cookie-parser";
     
    async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        app.useGlobalPipes(new ValidationPipe());
        app.use(cookieParser());
        await app.listen(3000);
    }
    bootstrap();

    Setup env variables

    .env
    DATABASE_URL="postgresql://username:password@localhost:5432/RollYourOwnAuth"
     
    NODE_ENV="development"
     
    JWT_ACCESS_TOKEN_SECRET="" # Run `openssl rand -base64 32` in your CLI to generate a secret
    JWT_REFRESH_TOKEN_SECRET="" # Run `openssl rand -base64 32` in your CLI to generate a secret
    JWT_ACCESS_EXPIRES_IN=30m
    JWT_REFRESH_EXPIRES_IN=30d
     
    ACCESS_TOKEN_COOKIE_MAX_AGE=1800000
    REFRESH_TOKEN_COOKIE_NAME=__refreshToken__
    REFRESH_TOKEN_COOKIE_MAX_AGE=2592000000
    ACCESS_TOKEN_COOKIE_NAME=__accessToken__

    Prisma config

    config/index.ts
    import { PrismaClient } from "@prisma/client";
     
    export const prisma = new PrismaClient();

    App module

    app.module.ts
    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 { ConfigModule } from "@nestjs/config";
     
    @Module({
        imports: [
            AuthModule,
            UsersModule,
            ConfigModule.forRoot({
                isGlobal: true,
            }),
        ],
        controllers: [],
        providers: [],
    })
    export class AppModule {}

    Create user dto

    users/dto/create.user.dto.ts
    import { IsString, IsEmail, MinLength, ValidateIf, IsIn, IsDefined } from "class-validator";
     
    export class CreateUserDto {
        @IsString({ message: "Full name is required" })
        name: string;
     
        @IsEmail({}, { message: "Not a valid email" })
        email: string;
     
        @IsString({ message: "Password is required" })
        @MinLength(6, { message: "Password must be at least 6 characters long" })
        password: string;
     
        @IsString()
        @IsDefined()
        @IsIn([Math.random()], {
            message: "Passwords do not match",
        })
        @ValidateIf((o) => o.password !== o.confirmPassword)
        confirmPassword: string;
    }

    Users module

    users/users.module.ts
    import { Module } from "@nestjs/common";
    import { UsersService } from "./users.service";
     
    @Module({
        providers: [UsersService],
    })
    export class UsersModule {}

    Users service

    users/users.service.ts
    import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
    import { prisma } from "src/config";
    import { CreateUserDto } from "./dto/create-user.dto";
    import * as argon2 from "argon2";
    import { Prisma, User } from "@prisma/client";
     
    @Injectable()
    export class UsersService {
        async createUser(userDetails: CreateUserDto): Promise<User> {
            const hashedPasword = await argon2.hash(userDetails.password);
     
            try {
                const user = await prisma.user.create({
                    data: {
                        name: userDetails.name,
                        email: userDetails.email,
                        password: hashedPasword,
                    },
                });
     
                return user;
            } catch (error) {
                if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
                    throw new HttpException("Email already exists", HttpStatus.CONFLICT);
                }
            }
        }
     
        async findUserById(id: string): Promise<User> {
            const user = await prisma.user.findUnique({
                where: {
                    id,
                },
            });
     
            return user;
        }
     
        async findUserByEmail(email: string): Promise<User> {
            const user = await prisma.user.findUnique({
                where: {
                    email,
                },
            });
     
            if (!user) {
                throw new HttpException("User not found", HttpStatus.NOT_FOUND);
            }
            return user;
        }
    }

    Extend express request type to include user info

    types/global.d.ts
    import { Request as ExpressRequest } from "express";
    declare module "express" {
        interface Request extends ExpressRequest {
            user: {
                id: string;
            };
        }
    }

    Auth module

    auth/auth.module.ts
    import { Module } from "@nestjs/common";
    import { AuthController } from "./auth.controller";
    import { AuthService } from "./auth.service";
    import { UsersService } from "src/users/users.service";
     
    @Module({
        controllers: [AuthController],
        providers: [AuthService, UsersService],
    })
    export class AuthModule {}

    Auth guard

    auth/auth.guard.ts
    import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
    import { ConfigService } from "@nestjs/config";
    import { Request } from "express";
    import * as jwt from "jsonwebtoken";
     
    @Injectable()
    export class AuthGuard implements CanActivate {
        constructor(private configService: ConfigService) {}
     
        async canActivate(context: ExecutionContext): Promise<boolean> {
            const request: Request = context.switchToHttp().getRequest();
            const refreshTokenCookieName = this.configService.get<string>("ACCESS_TOKEN_COOKIE_NAME");
     
            const token: string = request.cookies[refreshTokenCookieName];
            try {
                const deodedPayload = jwt.verify(
                    token,
                    this.configService.get<string>("JWT_ACCESS_TOKEN_SECRET")
                ) as Request["user"];
     
                // 💡 We're assigning the payload to the request object here
                // so that we can access it in our route handlers
                // 💡 and to make it typesafe we extended express request type in 'src/types/global.d.ts"
                request.user = deodedPayload;
            } catch {
                throw new UnauthorizedException();
            }
            return true;
        }
    }

    Login dto

    auth/dto/login.dto.ts
    import { IsString, IsEmail } from "class-validator";
     
    export class LoginDto {
        @IsEmail({}, { message: "Not a valid email" })
        email: string;
     
        @IsString({ message: "Password is required" })
        password: string;
    }

    Auth controller

    auth/auth.controller.ts
    import { Body, Controller, Get, HttpCode, HttpStatus, Post, Req, Res, UseGuards } from "@nestjs/common";
    import { AuthService } from "./auth.service";
    import { CreateUserDto } from "src/users/dto/create-user.dto";
    import { Request, Response } from "express";
    import { LoginDto } from "./dto/login.dto";
    import { ConfigService } from "@nestjs/config";
    import { AuthGuard } from "./auth.guard";
    import { UsersService } from "src/users/users.service";
     
    @Controller("auth")
    export class AuthController {
        constructor(
            private readonly authService: AuthService,
            private readonly configService: ConfigService,
            private readonly usersService: UsersService
        ) {}
     
        @Post("signup")
        @HttpCode(HttpStatus.CREATED)
        async signup(@Body() createUserDto: CreateUserDto, @Res({ passthrough: true }) res: Response) {
            const { access_token, refresh_token } = await this.authService.signup(createUserDto);
     
            res.cookie(this.configService.get<string>("REFRESH_TOKEN_COOKIE_NAME"), refresh_token, {
                secure: this.configService.get<string>("NODE_ENV") === "production",
                httpOnly: true,
                sameSite: "lax",
                maxAge: this.configService.get<number>("REFRESH_TOKEN_COOKIE_MAX_AGE"),
            });
     
            res.cookie(this.configService.get<string>("ACCESS_TOKEN_COOKIE_NAME"), access_token, {
                secure: this.configService.get<string>("NODE_ENV") === "production",
                httpOnly: true,
                sameSite: "lax",
                maxAge: this.configService.get<number>("ACCESS_TOKEN_COOKIE_MAX_AGE"),
            });
     
            return { access_token };
        }
     
        @Post("login")
        @HttpCode(HttpStatus.OK)
        async login(@Body() loginDto: LoginDto, @Res({ passthrough: true }) res: Response) {
            const { access_token, refresh_token } = await this.authService.login(loginDto);
     
            res.cookie(this.configService.get<string>("REFRESH_TOKEN_COOKIE_NAME"), refresh_token, {
                secure: this.configService.get<string>("NODE_ENV") === "production",
                httpOnly: true,
                sameSite: "lax",
                maxAge: this.configService.get<number>("REFRESH_TOKEN_COOKIE_MAX_AGE"),
            });
     
            res.cookie(this.configService.get<string>("ACCESS_TOKEN_COOKIE_NAME"), access_token, {
                secure: this.configService.get<string>("NODE_ENV") === "production",
                httpOnly: true,
                sameSite: "lax",
                maxAge: this.configService.get<number>("ACCESS_TOKEN_COOKIE_MAX_AGE"),
            });
     
            return { access_token };
        }
     
        @Post("refresh")
        @HttpCode(HttpStatus.OK)
        async refresh(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
            const old_refresh_token: string = req.cookies[this.configService.get<string>("REFRESH_TOKEN_COOKIE_NAME")];
     
            if (!old_refresh_token) {
                res.status(HttpStatus.UNAUTHORIZED).json({ message: "Unauthorized!" });
            }
     
            const { access_token, refresh_token: new_refresh_token } = await this.authService.refresh(old_refresh_token);
     
            res.cookie(this.configService.get<string>("REFRESH_TOKEN_COOKIE_NAME"), new_refresh_token, {
                secure: this.configService.get<string>("NODE_ENV") === "production",
                httpOnly: true,
                sameSite: "lax",
                maxAge: this.configService.get<number>("REFRESH_TOKEN_COOKIE_MAX_AGE"),
            });
     
            res.cookie(this.configService.get<string>("ACCESS_TOKEN_COOKIE_NAME"), access_token, {
                secure: this.configService.get<string>("NODE_ENV") === "production",
                httpOnly: true,
                sameSite: "lax",
                maxAge: this.configService.get<number>("ACCESS_TOKEN_COOKIE_MAX_AGE"),
            });
     
            return { access_token };
        }
     
        @Get("logout")
        @HttpCode(HttpStatus.OK)
        async logout(@Res({ passthrough: true }) res: Response) {
            res.clearCookie(this.configService.get<string>("ACCESS_TOKEN_COOKIE_NAME"))
                .clearCookie(this.configService.get<string>("REFRESH_TOKEN_COOKIE_NAME"))
                .end();
        }
     
        @UseGuards(AuthGuard)
        @Get("profile")
        @HttpCode(HttpStatus.OK)
        async profile(@Req() req: Request) {
            const { id, email, name } = await this.usersService.findUserById(req.user.id);
     
            return { id, email, name };
        }
    }

    Auth service

    auth/auth.service.ts
    import { Injectable } from "@nestjs/common";
    import { UsersService } from "src/users/users.service";
    import * as jwt from "jsonwebtoken";
    import * as argon2 from "argon2";
    import { ConfigService } from "@nestjs/config";
    import { CreateUserDto } from "src/users/dto/create-user.dto";
    import { LoginDto } from "./dto/login.dto";
    import { Request } from "express";
     
    @Injectable()
    export class AuthService {
        constructor(
            private readonly usersService: UsersService,
            private readonly configService: ConfigService
        ) {}
     
        async signup(userDetails: CreateUserDto) {
            const user = await this.usersService.createUser(userDetails);
     
            const access_token = jwt.sign({ id: user.id }, this.configService.get<string>("JWT_ACCESS_TOKEN_SECRET"), {
                expiresIn: this.configService.get<string>("JWT_ACCESS_EXPIRES_IN"),
            });
     
            const refresh_token = jwt.sign({ id: user.id }, this.configService.get<string>("JWT_REFRESH_TOKEN_SECRET"), {
                expiresIn: this.configService.get<string>("JWT_REFRESH_EXPIRES_IN"),
            });
     
            return { access_token, refresh_token };
        }
     
        async login(credentials: LoginDto) {
            const user = await this.usersService.findUserByEmail(credentials.email);
     
            await argon2.verify(user.password, credentials.password).catch();
     
            const access_token = jwt.sign({ id: user.id }, this.configService.get<string>("JWT_ACCESS_TOKEN_SECRET"), {
                expiresIn: this.configService.get<string>("JWT_ACCESS_EXPIRES_IN"),
            });
     
            const refresh_token = jwt.sign({ id: user.id }, this.configService.get<string>("JWT_REFRESH_TOKEN_SECRET"), {
                expiresIn: this.configService.get<string>("JWT_REFRESH_EXPIRES_IN"),
            });
     
            return { access_token, refresh_token };
        }
     
        async refresh(old_refresh_token: string) {
            const decoded = jwt.verify(
                old_refresh_token,
                this.configService.get<string>("JWT_REFRESH_TOKEN_SECRET")
            ) as Request["user"];
            const access_token = jwt.sign({ id: decoded.id }, this.configService.get<string>("JWT_ACCESS_TOKEN_SECRET"), {
                expiresIn: this.configService.get<string>("JWT_ACCESS_EXPIRES_IN"),
            });
     
            const refresh_token = jwt.sign({ id: decoded.id }, this.configService.get<string>("JWT_REFRESH_TOKEN_SECRET"), {
                expiresIn: this.configService.get<string>("JWT_REFRESH_EXPIRES_IN"),
            });
     
            return { access_token, refresh_token };
        }
    }

    Official example