サインアップ編
※ 簡単なUser情報を入力してサインアップを行うことを想定しています。
-
JwtModuleをAuthModuleにimportする。
- AuthModuleは認証用のモジュールで、実際のプロジェクトによってモジュール名は異なる場合があるので注意。
import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/prisma/prisma.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [PrismaModule, JwtModule.register({})], controllers: [AuthController], providers: [AuthService], }) export class AuthModule {}
-
JWT用の環境変数を設定する。
.envJWT_SECRET="test"
-
DTOを作成する。
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; export class AuthDto { @IsEmail() @IsNotEmpty() email: string; @IsString() @IsNotEmpty() @MinLength(5) password: string; }
-
インターフェースを作成する。
export interface Jwt { accessToken: string; }
-
認証サービスを作成する。
import { Injectable, ForbiddenException } from '@nestjs/common'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { PrismaService } from '../prisma/prisma.service'; import { AuthDto } from './dto/auth.dto'; @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly jwt: JwtService, private readonly config: ConfigService, ) {} async signUp(dto: AuthDto): void { const hashed = await bcrypt.hash(dto.password, 12); try { return await this.prisma.user.create({ data: { email: dto.email, hashedPassword: hashed, }, });; } catch (error) { if (error instanceof PrismaClientKnownRequestError) { if (error.code === 'P2002') { throw new ForbiddenException('This email is already taken'); } } throw error; } } }
-
コントローラーを実装する。
import { Controller, Post, Body, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthDto } from './dto/auth.dto'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('signup') signUp(@Body() dto: AuthDto): void { return this.authService.signUp(dto); } }
ログイン編
※ 「サインアップ編」が完了しているものとする。
※ cookieにJWTトークンを格納することを想定する。
-
サービスにログイン用の関数を定義する。
import { Injectable, ForbiddenException } from '@nestjs/common'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { PrismaService } from '../prisma/prisma.service'; import { AuthDto } from './dto/auth.dto'; import { Jwt } from './interfaces/auth.interface'; @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly jwt: JwtService, private readonly config: ConfigService, ) {} ... async login(dto: AuthDto): Promise<Jwt> { const user = await this.prisma.user.findUnique({ where: { email: dto.email, }, }); if (!user) throw new ForbiddenException('Email or password incorrect'); const isValid = await bcrypt.compare(dto.password, user.hashedPassword); if (!isValid) throw new ForbiddenException('Email or password incorrect'); return this.generateJwt(user.id, user.email); } private async generateJwt(userId: number, email: string): Promise<Jwt> { const payload = { sub: userId, email, }; const secret = this.config.get('JWT_SECRET'); const token = await this.jwt.signAsync(payload, { expiresIn: '5m', secret: secret, }); return { accessToken: token, }; } }
-
コントローラーにログイン用のエンドポイントを追加する。
import { Controller, Post, Body, HttpCode, HttpStatus, Res, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthDto } from './dto/auth.dto'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} ... @HttpCode(HttpStatus.OK) @Post('login') async login( @Body() dto: AuthDto, @Res({ passthrough: true }) res: Response, ): void { const jwt = await this.authService.login(dto); return res.cookie('access_token', jwt.accessToken, { httpOnly: true, secure: true, sameSite: 'none', path: '/', }); } }
API認証編
※ user更新APIを実装する。
-
DTOを作成する。
import { IsNotEmpty, IsString } from 'class-validator'; export class UpdateUserDto { @IsString() @IsNotEmpty() email: string; }
-
サービスにユーザー更新用の関数を定義する。
import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from '@prisma/client'; @Injectable() export class UserService { constructor(private prisma: PrismaService) {} async updateUser( userId: number, dto: UpdateUserDto, ): Promise<Omit<User, 'hashedPassword'>> { const user = await this.prisma.user.update({ where: { id: userId, }, data: { ...dto, }, }); delete user.hashedPassword; return user; } }
-
JWTストラテジーを作成する。
import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { PrismaService } from '../../prisma/prisma.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { constructor( private readonly config: ConfigService, private readonly prisma: PrismaService, ) { super({ jwtFromRequest: ExtractJwt.fromExtractors([ (req) => { let jwt = null; if (req && req.cookies) { jwt = req.cookies['access_token']; } return jwt; }, ]), ignoreExpiration: false, secretOrKey: config.get('JWT_SECRET'), }); } async validate(payload: { sub: number; email: string }) { const user = await this.prisma.user.findUnique({ where: { id: payload.sub, }, }); delete user.hashedPassword; return user; } }
-
コントローラーにユーザー更新用のエンドポイントを追加する。
import { Body, Controller, Patch, Req, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { UserService } from './user.service'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from '@prisma/client'; @UseGuards(AuthGuard('jwt')) @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} @Patch() updateUser( @Req() req: Request, @Body() dto: UpdateUserDto, ): Promise<Omit<User, 'hashedPassword'>> { return this.userService.updateUser(req.user.id, dto); } }
-
JwtStrategyをAuthModuleに読み込ませる。
import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/prisma/prisma.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './strategy/jwt.strategy'; @Module({ imports: [PrismaModule, JwtModule.register({})], controllers: [AuthController], providers: [AuthService, JwtStrategy], }) export class AuthModule {}
ログアウト編
-
コントローラーにログアウト用のエンドポイントを追加する。
import { Controller, Post, Body, HttpCode, HttpStatus, Res, Req, Get, } from '@nestjs/common'; import { Request, Response } from 'express'; import { AuthService } from './auth.service'; import { AuthDto } from './dto/auth.dto'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} ... @HttpCode(HttpStatus.OK) @Post('/logout') logout(@Req() req: Request, @Res({ passthrough: true }) res: Response): void { return res.cookie('access_token', '', { httpOnly: true, secure: true, sameSite: 'none', path: '/', }); } }
CSRFトークン設定編
-
csurfの設定を行う。
main.tsimport { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { Request } from 'express'; import * as cookieParser from 'cookie-parser'; import * as csurf from 'csurf'; async function bootstrap() { const app = await NestFactory.create(AppModule); ... app.use( csurf({ cookie: { httpOnly: true, sameSite: 'none', secure: true, }, value: (req: Request) => { return req.header('csrf-token'); }, }), ); await app.listen(process.env.PORT || 3005); } bootstrap();
-
CSRFトークンを返すエンドポイントを追加する。
import { Controller, Post, Body, HttpCode, HttpStatus, Res, Req, Get, } from '@nestjs/common'; import { Request, Response } from 'express'; import { AuthService } from './auth.service'; import { AuthDto } from './dto/auth.dto'; import { Csrf } from './interfaces/auth.interface'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Get('/csrf') getCsrfToken(@Req() req: Request): Csrf { return { csrfToken: req.csrfToken() }; } ... }
-
APIを叩く場合はヘッダー情報にCSRFトークンを設定するようにする。
csrf-token: 受け取ったCSRFトークン
参考
Guards | NestJS
Guards | NestJS 【翻訳】
NestJSでGuardを使って認可を実装する
Nest.jsでGuardからControllerへデータを渡す方法
NestJSのAuthGuard周りをアレンジしたり、深掘りしたり
NestJSでJWTを使った認証を実装する