🗝️

NestJSにおけるPassportとJWTでのユーザ認証をわかりやすく

に公開

はじめに

ウェブアプリケーションでユーザー認証を実装する際、NestJSではPassportとJWTの組み合わせがよく使われます。この記事では、これらの技術が何なのか、なぜ一緒に使うのが良いのかを、誰にでもわかるように説明します。

PassportとJWTの関係:遊園地の例え

想像してみてください。あなたが遊園地に入りたいとします。

パスポート(Passport)とは?

  • パスポートは「入場チェック係」のようなものです
  • いろんな種類の入場券を確認できる優れた係員です
  • 今回はJWTという特別な入場券を使うと決めています

JWT(Json Web Token)とは?

  • JWTは「特別な入場券」のようなもの
  • この入場券には、あなたの名前や有効期限などの情報が書かれています
  • 誰かが勝手に情報を書き換えると、すぐにわかる特殊な入場券です

なぜパスポート係員がいると便利なの?

もし入場券だけあって、チェック係がいなければ:

  • 誰が入場券をチェックするの?
  • エラーがあったらどうする?
  • 後で別の種類の入場券も使えるようにしたいときは?

パスポート係員がいると:

  1. 「この人は入っていいよ」「この人はダメ」をちゃんと判断してくれます
  2. 後で「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;
  },
);

実際の流れ(遊園地の例えで)

  1. 入場券を発行する (JWT作成)
    ユーザーがログインすると、AuthServiceのloginメソッドが「入場券」(JWT)を発行します。

  2. 入場券を持ってアトラクションに行く (保護されたAPIにアクセス)
    ユーザーがプロフィールページなどの保護されたリソースにアクセスするとき、「入場券」(JWT)を提示します。

  3. 入場チェック係が確認する (Passport+JWT検証)
    JwtStrategyのvalidateメソッドが「入場券」を確認し、有効であれば通してくれます。

  4. アトラクションを楽しむ (APIリソースにアクセス)
    認証が成功すれば、要求したリソースにアクセスできます。

まとめ

NestJSでPassportとJWTを組み合わせることで、柔軟で安全な認証システムを構築できます。

  • JWT:特別な情報が書かれた「入場券」
  • Passport:様々な種類の「入場券」を確認できる「チェック係」

この2つを組み合わせることで、シンプルかつ拡張性の高い認証システムを実現できます。将来的に認証方法を追加したいときも、Passportのおかげで簡単に対応できるでしょう。

Discussion