NestJSにおけるPassportとJWTでのユーザ認証をわかりやすく
はじめに
ウェブアプリケーションでユーザー認証を実装する際、NestJSではPassportとJWTの組み合わせがよく使われます。この記事では、これらの技術が何なのか、なぜ一緒に使うのが良いのかを、誰にでもわかるように説明します。
PassportとJWTの関係:遊園地の例え
想像してみてください。あなたが遊園地に入りたいとします。
パスポート(Passport)とは?
- パスポートは「入場チェック係」のようなものです
- いろんな種類の入場券を確認できる優れた係員です
- 今回はJWTという特別な入場券を使うと決めています
JWT(Json Web Token)とは?
- JWTは「特別な入場券」のようなもの
- この入場券には、あなたの名前や有効期限などの情報が書かれています
- 誰かが勝手に情報を書き換えると、すぐにわかる特殊な入場券です
なぜパスポート係員がいると便利なの?
もし入場券だけあって、チェック係がいなければ:
- 誰が入場券をチェックするの?
- エラーがあったらどうする?
- 後で別の種類の入場券も使えるようにしたいときは?
パスポート係員がいると:
- 「この人は入っていいよ」「この人はダメ」をちゃんと判断してくれます
- 後で「LINEログインの入場券」や「Googleログインの入場券」なども受け付けたくなったとき、新しい係員を雇う必要がなく、同じパスポート係員に「これも確認してね」と言うだけでOKです
つまり、JWTだけだと「入場券」だけがあって、それを誰がどう確認するかの仕組みがありません。パスポートがあることで、入場券の確認を簡単に管理できるようになります。
NestJSでのPassportとJWTの実装方法
それでは、実際にNestJSでPassportとJWTを使った認証システムを実装する方法を見ていきましょう。
1. 必要なパッケージのインストール
まずは必要なパッケージをインストールします。
npm install @nestjs/passport passport passport-jwt @nestjs/jwt jsonwebtoken
npm install -D @types/passport-jwt
2. 認証モジュールの作成
次に、認証モジュール(AuthModule)を作成します。
// auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { UserModule } from '../user/user.module';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';
@Module({
imports: [
UserModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
3. JWT戦略の実装
JWTを使った認証戦略を実装します。これが「入場券のチェック方法」を定義する部分です。
// jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from '../user/user.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private userService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
const user = await this.userService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('ユーザーが見つかりません');
}
return user;
}
}
4. 認証サービスの実装
ログインやJWTトークン発行機能を持つサービスを実装します。
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise<any> {
const user = await this.userService.findByUsername(username);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
5. コントローラーの実装
ログインAPIを提供するコントローラーを実装します。
// auth.controller.ts
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
async login(@Body() loginDto: { username: string; password: string }) {
const user = await this.authService.validateUser(
loginDto.username,
loginDto.password,
);
if (!user) {
throw new UnauthorizedException('ユーザー名またはパスワードが間違っています');
}
return this.authService.login(user);
}
}
6. 保護されたルートの作成
認証が必要なAPIエンドポイントを作成します。
// user.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { UserService } from './user.service';
import { GetUser } from '../auth/get-user.decorator';
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@GetUser() user) {
return user;
}
}
JWT認証ガードも作成します:
// jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
そして、現在のユーザーを取得するためのデコレータも作成します:
// get-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const GetUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
実際の流れ(遊園地の例えで)
-
入場券を発行する (JWT作成):
ユーザーがログインすると、AuthServiceのlogin
メソッドが「入場券」(JWT)を発行します。 -
入場券を持ってアトラクションに行く (保護されたAPIにアクセス):
ユーザーがプロフィールページなどの保護されたリソースにアクセスするとき、「入場券」(JWT)を提示します。 -
入場チェック係が確認する (Passport+JWT検証):
JwtStrategyのvalidate
メソッドが「入場券」を確認し、有効であれば通してくれます。 -
アトラクションを楽しむ (APIリソースにアクセス):
認証が成功すれば、要求したリソースにアクセスできます。
まとめ
NestJSでPassportとJWTを組み合わせることで、柔軟で安全な認証システムを構築できます。
- JWT:特別な情報が書かれた「入場券」
- Passport:様々な種類の「入場券」を確認できる「チェック係」
この2つを組み合わせることで、シンプルかつ拡張性の高い認証システムを実現できます。将来的に認証方法を追加したいときも、Passportのおかげで簡単に対応できるでしょう。
Discussion