Closed7

NestJS触ってみる(JWT認証)

keisuke-333keisuke-333

セットアップ

Auth モジュールを作成

terminal
$ yarn nest g module auth

↪︎ src/auth/auth.module.ts が作成
↪︎ app.module.tsimportsAuthModule が追加

Auth コントローラーを作成

terminal
$ yarn nest g controller auth --no-spec

↪︎ src/auth/auth.controller.ts が作成
↪︎ auth.module.tscontrollersAuthController が追加

Auth サービスを作成

terminal
$ yarn nest g service auth --no-spec

↪︎ src/auth/auth.service.ts が作成
↪︎ auth.module.tsprovidersAuthService が追加

keisuke-333keisuke-333

usersテーブルの作成

schema.prismaに User モデルを定義

schema.prisma
// --------- 略 ---------
model User {
  id         String     @id @default(uuid())
  name       String     @unique
  password   String
  status     UserStatus
  created_at DateTime   @default(dbgenerated("NOW()")) @db.Timestamp(0)
  updated_at DateTime   @default(dbgenerated("NOW() ON UPDATE CURRENT_TIMESTAMP")) @db.Timestamp(0)

  @@map("users")
}

enum UserStatus {
  FREE    // 無料会員
  PREMIUM // 有料会員
}
// --------- 略 ---------

マイグレーションファイル作成とテーブル作成

terminal
$ yarn prisma migrate dev --name create_users_table
keisuke-333keisuke-333

ユーザー作成機能の実装

DTOクラスの作成(バリデーション)

src
└── auth
    └── dto
        └── create-user.dto.ts ←新規作成
create-user.dto.ts
import { UserStatus } from '@prisma/client'
import {
  IsEnum,
  IsNotEmpty,
  IsString,
  MaxLength,
  MinLength,
} from 'class-validator'

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string

  @IsString()
  @MinLength(8)
  @MaxLength(32)
  password: string

  @IsEnum(UserStatus)
  status: UserStatus
}

ユーザー作成機能

auth.module.tsPrismaService を追加

auth.module.ts
import { Module } from '@nestjs/common'
+ import { PrismaService } from 'src/prisma.service'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'

@Module({
  controllers: [AuthController],
- providers: [AuthService],
+ providers: [AuthService, PrismaService],
})
export class AuthModule {}

auth.service.tssignUpメソッドを実装

auth.service.ts
import { Injectable } from '@nestjs/common'
import { Prisma, User } from '@prisma/client'
import { PrismaService } from 'src/prisma.service'
import { CreateUserDto } from './dto/create-user.dto'

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService) {}

  async signUp(createUserDto: CreateUserDto): Promise<User> {
    const { name, password } = createUserDto
    const data: Prisma.UserCreateInput = {
      name,
      password,
      status: 'FREE',
    }
    return this.prisma.user.create({ data })
  }
}

auth.controller.ts@Post を追加

auth.controller.ts
import { Body, Controller, Post } from '@nestjs/common'
import { User } from '@prisma/client'
import { AuthService } from './auth.service'
import { CreateUserDto } from './dto/create-user.dto'

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('signup')
  async signup(@Body() createUserDto: CreateUserDto): Promise<User> {
    return this.authService.signUp(createUserDto)
  }
}

データベースに登録できるか確認

terminal
$ curl -X POST -d "name=山田 太郎&password=test1234&status=FREE" http://localhost:3000/auth/signup | jq

{
  "id": "a7c9507c-6add-4b82-bdd6-d8c2………",
  "name": "山田 太郎",
  "password": "test1234",
  "status": "FREE",
  "created_at": "2022-04-11T18:16:55.000Z",
  "updated_at": "2022-04-11T18:16:55.000Z"
}

パスワードのハッシュ化

bcrypt

terminal
$ yarn add bcrypt
$ yarn add --dev @types/bcrypt

auth.service.ts を編集

auth.service.ts
import { Injectable } from '@nestjs/common'
import { Prisma, User } from '@prisma/client'
+ import * as bcrypt from 'bcrypt'
import { PrismaService } from 'src/prisma.service'
import { CreateUserDto } from './dto/create-user.dto'

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService) {}

  async createUser(createUserDto: CreateUserDto): Promise<User> {
    const { name, password, status } = createUserDto
+   const salt = await bcrypt.genSalt()
+   const hashPassword = await bcrypt.hash(password, salt)
    const data: Prisma.UserCreateInput = {
      name,
-     password,
+     password: hashPassword,
      status,
    }
    return this.prisma.user.create({ data })
  }
}

データベースにハッシュ化されて保存されるか確認

terminal
$ curl -X POST -d "name=山田 二郎&password=test1234&status=FREE" http://localhost:3000/auth/signup | jq

{
  "id": "be06c8b1-7542-4c28-8340-97587ace801e",
  "name": "山田 二郎",
  "password": "$2b$10$r9CI3pNgnlJYRT………",
  "status": "FREE",
  "created_at": "2022-04-12T11:50:09.000Z",
  "updated_at": "2022-04-12T11:50:09.000Z"
}
keisuke-333keisuke-333

JWTの導入

terminal
$ yarn add @nestjs/jwt @nestjs/passport passport passport-jwt
$ yarn add --dev @types/passport-jwt

passport
↪︎ Node.js のための認証ミドルウェア

passport-jwt
↪︎ JWT の検証処理用


JWTの設定

.envファイルに JWT_SECRET_KEY を追加

.env
JWT_SECRET_KEY="secretKey123"

@nestjs/configパッケージを使用して環境変数を読み込む

terminal
$ yarn add @nestjs/config

app.module.ts.envファイルを読み込む

app.module.ts
import { Module } from '@nestjs/common'
+ import { ConfigModule } from '@nestjs/config'
import { ItemsModule } from './items/items.module'
import { AuthModule } from './auth/auth.module'

@Module({
- imports: [ItemsModule, AuthModule],
+ imports: [
+  ConfigModule.forRoot({ isGlobal: true }),
+  ItemsModule,
+  AuthModule
+ ],
  controllers: [],
  providers: [],
})
export class AppModule {}

auth.module.ts を編集

auth.module.ts
import { Module } from '@nestjs/common'
+ import { ConfigService } from '@nestjs/config'
+ import { JwtModule } from '@nestjs/jwt'
+ import { PassportModule } from '@nestjs/passport'
import { PrismaService } from 'src/prisma.service'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'

@Module({
+ imports: [
+   PassportModule.register({ defaultStrategy: 'jwt' }),
+   JwtModule.registerAsync({
+     useFactory: async (configService: ConfigService) => {
+       return {
+         secret: configService.get<string>('JWT_SECRET_KEY'),
+         signOptions: {
+           expiresIn: 3600,
+         },
+       }
+     },
+     inject: [ConfigService],
+   }),
+ ],
  controllers: [AuthController],
  providers: [AuthService, PrismaService],
})
export class AuthModule {}

keisuke-333keisuke-333

ログイン機能

ユーザー認証用の DTO クラスを作成

src
└── auth
    └── dto
        ├── create-user.dto.ts
        └── credentials.dto.ts ←新規作成
credentials.dto.ts
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'

export class CredentialsDto {
  @IsString()
  @IsNotEmpty()
  name: string

  @IsString()
  @MinLength(8)
  @MaxLength(32)
  password: string
}

ログイン機能の実装

auth.service.tssignInメソッドを追加

auth.service.ts
// --------- 略 ---------
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { CredentialsDto } from './dto/credentials.dto'

@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService
  ) {}

  // --------- 略 ---------

  async signIn(
    credentialsDto: CredentialsDto,
  ): Promise<{ accessToken: string }> {
    const { name, password } = credentialsDto
    const user = await this.prisma.user.findUnique({
      where: { name },
    })
    if (user && (await bcrypt.compare(password, user.password))) {
      const payload = { id: user.id, name: user.name }
      const accessToken = this.jwtService.sign(payload)
      return { accessToken }
    }
    throw new UnauthorizedException(
      'ユーザー名またはパスワードを確認してください',
    )
  }
}

auth.controller.tssighIn コントローラーを追加

auth.controller.ts
// --------- 略 ---------
import { CredentialsDto } from './dto/credentials.dto'

@Controller('auth')
export class AuthController {
// --------- 略 ---------
  @Post('signin')
  async signIn(
    @Body() credentialsDto: CredentialsDto,
  ): Promise<{ accessToken: string }> {
    return this.authService.signIn(credentialsDto)
  }
}

動作確認

間違ったパスワードでエラーが返ってくるか確認

terminal
$ curl -X POST -d "name=山田 二郎&password=test123123" http://localhost:3000/auth/signin | jq

{
  "statusCode": 401,
  "message": "ユーザー名またはパスワードを確認してください",
  "error": "Unauthorized"
}

正しいパスワードで accessToken が返ってくるか確認

terminal
$ curl -X POST -d "name=山田 二郎&password=test1234" http://localhost:3000/auth/signin | jq

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR………"
}

JWTトークンの確認
↪︎ jwt.io

このスクラップは2022/04/18にクローズされました