1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Nest.JSでJWT認証機能を実装する

Posted at

サインアップ編

※ 簡単なUser情報を入力してサインアップを行うことを想定しています。

  1. 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 {}
    
  2. JWT用の環境変数を設定する。

    .env
    JWT_SECRET="test"
    
  3. DTOを作成する。

    import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
    
    export class AuthDto {
      @IsEmail()
      @IsNotEmpty()
      email: string;
    
      @IsString()
      @IsNotEmpty()
      @MinLength(5)
      password: string;
    }
    
  4. インターフェースを作成する。

    export interface Jwt {
      accessToken: string;
    }
    
  5. 認証サービスを作成する。

    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;
        }
      }
    }
    
  6. コントローラーを実装する。

    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トークンを格納することを想定する。

  1. サービスにログイン用の関数を定義する。

    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,
        };
      }
    }
    
  2. コントローラーにログイン用のエンドポイントを追加する。

    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を実装する。

  1. DTOを作成する。

    import { IsNotEmpty, IsString } from 'class-validator';
    
    export class UpdateUserDto {
      @IsString()
      @IsNotEmpty()
      email: string;
    }
    
  2. サービスにユーザー更新用の関数を定義する。

    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;
      }
    }
    
  3. 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;
      }
    }
    
  4. コントローラーにユーザー更新用のエンドポイントを追加する。

    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);
      }
    }
    
  5. 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 {}
    

ログアウト編

  1. コントローラーにログアウト用のエンドポイントを追加する。

    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トークン設定編

  1. csurfの設定を行う。

    main.ts
    import { 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();
    
  2. 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() };
      }
    
      ...
    }
    
  3. APIを叩く場合はヘッダー情報にCSRFトークンを設定するようにする。

    csrf-token: 受け取ったCSRFトークン
    

参考

Guards | NestJS
Guards | NestJS 【翻訳】
NestJSでGuardを使って認可を実装する
Nest.jsでGuardからControllerへデータを渡す方法
NestJSのAuthGuard周りをアレンジしたり、深掘りしたり
NestJSでJWTを使った認証を実装する

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?