NestJS で認証する
NestJS で passport を使う。
passport | NestJS - A progressive Node.js framework
パッケージをインストールする
| $ npm i @nestjs/passport passport passport-local
$ npm i -D @types/passport-local
|
パッケージをインストールする
| $ npm i @nestjs/jwt passport-jwt
$ npm i -D @types/passport-jwt
|
| $ npm i bcrypt
$ npm i -D @types/bcrypt
|
認証リソースを作成する
| $ npx nest g resource
? What name would you like to use for this resource (plural, e.g., "users")? auth
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? No
CREATE src/auth/auth.controller.spec.ts (556 bytes)
CREATE src/auth/auth.controller.ts (204 bytes)
CREATE src/auth/auth.module.ts (241 bytes)
CREATE src/auth/auth.service.spec.ts (446 bytes)
CREATE src/auth/auth.service.ts (88 bytes)
UPDATE src/app.module.ts (1693 bytes)
|
Service
Service でアカウント名パスワードの確認を行い、ユーザーを返す。
| src/auth/auth.service.ts |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93 | import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { InjectRepository } from "@nestjs/typeorm";
import * as bcrypt from "bcrypt";
import { User } from "src/users/entities/user.entity";
import { UsersService } from "src/users/users.service";
import { Repository } from "typeorm";
import { CreateAuthDto } from "./dto/create-auth.dto";
import { UpdatePasswordDto } from "./dto/update-password.dto";
import { Auth } from "./entities/auth.entity";
import { AuthNotFoundError } from "./errors/auth-not-found-error";
import { PasswordIncorrectError } from "./errors/password-incorrect-error";
import { LogInResult } from "./interfaces/log-in-result";
import { Payload } from "./interfaces/payload";
// ログイン処理を行う
@Injectable()
export class AuthService {
private readonly saltOrRounds = 10;
constructor(
private readonly jwtService: JwtService,
@InjectRepository(Auth)
private readonly authRepository: Repository<Auth>,
private readonly usersService: UsersService
) {}
async validate(accountName: string, password: string): Promise<User> {
const auth = await this.findOne(accountName);
if (!(await bcrypt.compare(password, auth.hashed))) {
throw new PasswordIncorrectError();
}
return this.usersService.findOne(auth.userId);
}
async logIn(user: User): Promise<LogInResult> {
const payload: Payload = { userId: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
async create(createAuthDto: CreateAuthDto): Promise<Auth> {
const hashed = await this.getHashed(createAuthDto.password);
await this.authRepository.save({
accountName: createAuthDto.accountName,
hashed,
userId: createAuthDto.userId,
});
const auth = await this.authRepository.findOneBy({
accountName: createAuthDto.accountName,
});
return auth;
}
async findOne(accountName: string): Promise<Auth> {
const auth = await this.authRepository.findOneBy({ accountName });
if (!auth) {
throw new AuthNotFoundError();
}
return auth;
}
async updatePassword(
accountName: string,
updatePasswordDto: UpdatePasswordDto
): Promise<void> {
const auth = await this.authRepository.findOneBy({ accountName });
if (!auth) {
throw new AuthNotFoundError();
}
if (
!(await bcrypt.compare(updatePasswordDto.currentPassword, auth.hashed))
) {
throw new PasswordIncorrectError();
}
const hashed = await this.getHashed(updatePasswordDto.password);
await this.authRepository.update(accountName, { hashed });
}
async remove(accountName: string): Promise<void> {
const auth = await this.authRepository.findOneBy({ accountName });
if (!auth) {
throw new AuthNotFoundError();
}
await this.authRepository.delete(accountName);
}
private getHashed(password: string): Promise<string> {
return bcrypt.hash(password, this.saltOrRounds);
}
}
|
| src/auth/interfaces/log-in-result.ts |
|---|
| export interface LogInResult {
access_token: string;
}
|
| src/auth/interfaces/payload.ts |
|---|
| export interface Payload {
userId: string;
}
|
LocalStrategy
| src/auth/local.strategy.ts |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { User } from "src/users/entities/user.entity";
import { AuthService } from "./auth.service";
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
// Passport.jsは認証に使うフィールド名がusername、passwordなので変える
super({ usernameField: "accountName", passwordField: "password" });
}
async validate(accountName: string, password: string): Promise<User> {
try {
const user = await this.authService.validate(accountName, password);
return user;
} catch (error) {
throw new UnauthorizedException();
}
}
}
|
Module
| src/auth/auth.module.ts |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UsersModule } from "src/users/users.module";
import { jwtConstants } from "./auth.constants";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { Auth } from "./entities/auth.entity";
import { JwtStrategy } from "./jwt.strategy";
import { LocalStrategy } from "./local.strategy";
@Module({
imports: [
TypeOrmModule.forFeature([Auth]),
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: parseInt(process.env.JWT_EXPIRES_IN) },
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
|
Controller
認証の入口を作成する。
| src/auth/local-auth.guard.ts |
|---|
| import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class LocalAuthGuard extends AuthGuard("local") {}
|
| src/auth/auth.controller.ts |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | import { Controller, Post, Request, UseGuards } from "@nestjs/common";
import { User } from "src/users/entities/user.entity";
import { AuthService } from "./auth.service";
import { LocalAuthGuard } from "./local-auth.guard";
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
// LocalAuthGuard経由でLocalStrategy.validate()が実行される。
// AuthServiceでアカウント名とパスワードの一致を確認し、reqオブジェクトにuserが返ってくる。
@UseGuards(LocalAuthGuard)
@Post("log-in")
async login(@Request() req) {
const user: User = req.user;
return this.authService.logIn(user);
}
}
|
ここまでで認証できるはず。以降でリクエストに乗ってきた JWT を検証する。
JWT 生成のシークレットは公開しないように環境変数を参照するようにする。
| src/auth/auth.constants.ts |
|---|
| export const jwtConstants = {
secret: process.env.JWT_SECRET,
};
|
| src/auth/jwt.strategy.ts |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { jwtConstants } from "./auth.constants";
import { Payload } from "./interfaces/payload";
// クライアントから受け取ったJWTが有効かを確認する
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
// JWTの抽出方法
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// Passport.jsにJWTの期限切れを確認させる
ignoreExpiration: false,
// シークレット
secretOrKey: jwtConstants.secret,
});
}
// Payloadのバリデーション処理をここに書く
async validate(payload: Payload): Promise<Payload> {
return { userId: payload.userId };
}
}
|
| src/auth/jwt-auth.guard.ts |
|---|
| import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}
|
Controller のメソッドに@UseGuards(JwtAuthGuard)を付与することで認証するようになる。
JWT について
JWT はヘッダ、ペイロード、署名で構成されている。それぞれは.で結合されており、ヘッダ.ペイロード.署名という具合である。
JWT の実例を以下に挙げる。
| eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJhNzM3YzNhZS02MDc1LTQzNzQtYWNiZS03MDc5MTAzZmNkZTYiLCJpYXQiOjE3MjIyMTQzNjcsImV4cCI6MTcyMjIxNDk2N30.N2sCZDnn2_1yUmbawqBjOW6VS9hXFrodMvr1gDMmvu4
|
ペイロードには JavaScript のオブジェクトを含めることができる。
アクセストークンはユーザーがリソースにアクセスするために使われるトークンのこと。このアクセストークンに JWT が採用されることが多い。
環境変数で認証をスキップさせる
Guard のcanActivate()をオーバーライドし、環境変数が有効の場合はtrueを返却させれば強制的に認証 OK にできる。環境変数が無効の場合は親クラスのcanActivate()を呼び出すようにする。
| src/auth/jwt-auth.guard.ts |
|---|
1
2
3
4
5
6
7
8
9
10
11
12 | @Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
if (process.env.SKIP_AUTH === "true") {
console.log("Skip auth");
return true;
}
return super.canActivate(context);
}
}
|