NestJS, Prisma, PostgreSQL(Docker)で...Part3
認証つくっていくぜ
今回はNestJS公式で紹介されてるJWT認証を実装していきます
具体的には
-
JWT
を発行する(signin時) - リクエストのヘッダーを見て、有効な
JWT
が含まれているか?を確認するGuard
を作る - 実行前に
Guard
で正しいJWT
持ってますか?て確認をするAPIを作る
です。
JWTよくわからんて方は、わかりやすく解説してくれてる方がいたのでこちらをさっと読んでみてください
email, passwordでuserをfindする超簡単な認証を作る
AuthModule, AuthService, AuthController 生成
$ nest g module auth &&
nest g controller auth &&
nest g service auth
まずはもろもろ生成...
AuthModule
# auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { PrismaModule } from 'src/prisma/prisma.module'; //追加
@Module({
imports: [PrismaModule], //追加
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
PrismaModule
をimport
に追加してAuthService
内で使えるようにしておきます
UserModule
でもやったね!
AuthService
# auth.service.ts
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService) {}
async validate(
email: string,
password: string,
): Promise<Omit<User, 'password'> | null> {
const user = await this.prisma.user.findFirst({ where: { email } });
if (user && (await bcrypt.compare(password, user.password))) {
const result = { ...user };
delete result.password;
return result;
}
return null;
}
}
email
とpassword
を受け取り、User型
からpassword
を除外したObject
を返します
email
が一致するuser
が見つからなかった、もしくはuser
のpassword
が一致しなかった場合はnull
を返します
AuthController
- 受け取るデータの型を定義しておきます
# auth/dto/signin.dto.ts
export interface SigninDto {
email: string;
password: string;
}
- controller
# auth.controller.ts
import {
Body,
Controller,
Post,
HttpCode,
HttpStatus,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { SigninDto } from './dto/signin.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('signin')
async signIn(@Body() signInDto: SigninDto) {
const user = this.authService.validate(signInDto.email, signInDto.password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return user;
}
}
auth/signin
にPOST request
があったときにSigninDto型
のBody
を受け取り、
先ほど作成したvalidate
を呼び出し、問題なくuser
が返ってきた場合はそのまま返し、null
の場合は401 error
を返します
動作確認
今回も私はPostmanで
method: POST
URL: http://localhost:3000/auth/signin
Body: {
"email": "user1@example.com",
"password": "password"
}
よき(≧∇≦)b
JWT!!
まずはJWTを生成する処理を書いていきます
npm install
$ npm install --save @nestjs/jwt
tokenの生成、検証につかいやす
JWTの署名に必要なSecretを定義
# auth/constants.ts
export const jwtConstants = {
secret:
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};
access_tokenの生成
# auth.service.ts
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
略...
async signin(
user: Omit<User, 'password'>,
): Promise<{ access_token: string }> {
const payload = { sub: user.id, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload),
};
}
}
password
を除外したuser
を受け取って署名したtokenを発行して返します
# auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('signin')
async signIn(@Body() signInDto: SigninDto) {
const user = await this.authService.validate(
signInDto.email,
signInDto.password,
);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return this.authService.signin(user);
}
}
POST auth/signin
リクエストが来たら一致するuser
を探して問題なければ先ほど作成したJWT
を生成するauthService.signin
を呼んでそのまま返ってきたtokenを返します
# auth.module.ts
略...
@Module({
imports: [
PrismaModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
controllers: [AuthController],
providers: [AuthService],
})
-
JwtModule
をglobal
に設定してます - 定義した
Secret
を注入してあげます - tokenの有効期限を定義してます(60sはかなり短い)
動作確認
リクエストは先程同様で...
JWT返ってきましたね!
よきき(≧∇≦)b
Guard実装
リクエストが来たときにJWT
持ってる?それまだ有効?ただしいやつ?を確認する処理を書いていきます
AuthGuard実装
# auth/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
request['user'] = payload;
} catch (error) {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
private extractTokenFromHeader
関数はrequest
のAuthorizationヘッダー
からtokenを抽出して返します、無かったらundefined
を返します
上記を実行してトークンが取得できたら、そのトークンを検証します
有効であればrequest['user']
に保存してtrue
を返します。(この保存されたuser
は後ほど使います
AuthGuardを適用したエンドポイント
# auth.controller.ts
略...
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
GET auth/profile
リクエストが来た場合、AuthGuard
を通り、getProfile API
が実行されまマ先ほど保存したuser
(JWTから復元した内容)が返されます
動作確認
-
token
をheader
に含めないで保護されたエンドポイントを叩く
method: GET
URL: http://localhost:3000/auth/profile
結果: 401(未認証だよ😠)
-
token
をheader
に含めて保護されたエンドポイントを叩く
まずはJWT
を取得する
method: POST
URL: http://localhost:3000/auth/signin
Body: {
"email": "user1@example.com",
"password": "password"
}
JWT
をheader
に追加して叩き直す
(token
の有効期限が60s
なのでかなり急いだ
method: GET
URL: http://localhost:3000/auth/profile
header: {
Authorization: Bearer {取得したaccess_token}
}
AuthGuard
を問題なくPASSして、JWTのPayloadに設定した値が返ってきたね(^^)
乙!
これでJWTを使ってエンドポイントの保護と認証ができますた
localStorage
なりcookie
なりに保存してログイン状態を保持できてこそログイン機能作れた感あるよね。次回やりましょう!
Discussion