Nest.js で JWT 認証実装まとめ
この記事では、Nest.js のnest g resource
コマンドを使って認証モジュールを作成し、JWT(JSON Web Token)を使った認証機能を実装する方法をまとめました。
最近 Nest.js を触りはじめたので、知識の整理とメモとしてまとめました。
前提条件
- Node.js、npm/yarn がインストールされている
- Nest.js CLI がインストールされている(
npm i -g @nestjs/cli
) - Prisma が設定済み(データベース接続済み)
1. Auth リソースの生成
なぜ必要?
Nest.js の認証機能を効率的に開発するため、必要な基本ファイル構造を自動生成します。手動でファイルを作成するよりも、CLI を使うことで Nest.js の規約に従った構造を確実に作成でき、開発時間を大幅に短縮できます。
まず、Nest.js CLI を使って Auth モジュールを生成します:
nest g resource auth --no-spec
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes # ← 必ずYesを選択
重要: "Would you like to generate CRUD entry points?" の質問には必ず "Yes" と答えてください。No にすると、DTO や entity ファイルが生成されません。
このコマンドで以下のファイルが自動生成されます:
-
auth.controller.ts
- 認証エンドポイントを定義 -
auth.service.ts
- 認証ロジックを実装 -
auth.module.ts
- 認証モジュールの設定 -
dto/create-auth.dto.ts
- 一般的な CRUD 用(後で認証用に置き換え) -
dto/update-auth.dto.ts
- 一般的な CRUD 用(認証では削除) -
entities/auth.entity.ts
- データモデル(Prisma 使用のため削除)
トラブルシューティング:
もし DTO や entity ファイルが生成されない場合:
-
NestJS CLI を最新版に更新
npm install -g @nestjs/cli@latest npm install @nestjs/cli@latest
-
依存関係を再インストール
rm -rf node_modules package-lock.json npm install
-
再度リソースを生成 (前回生成したファイルは削除してから)
rm -rf src/auth nest g resource auth --no-spec
重要: 生成された DTO と entity ファイルは、一般的な CRUD 操作用のテンプレートなので、認証用途に合わせて置き換える必要があります。
2. 必要なパッケージのインストール
なぜ必要?
JWT 認証を実装するには、トークンの生成・検証、パスワードのハッシュ化、Passport 戦略など、専用のライブラリが必要です。これらのパッケージは認証のセキュリティを確保し、業界標準の実装を可能にします。
JWT 認証に必要なパッケージをインストールします:
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install -D @types/passport-jwt @types/bcrypt
-
@nestjs/jwt
- JWT トークンの生成・検証 -
@nestjs/passport
- Nest.js 用の Passport 統合 -
passport-jwt
- JWT 戦略の実装 -
bcrypt
- パスワードの安全なハッシュ化
3. 認証用 DTO の作成
なぜ必要?
nest g resource
で自動生成されたcreate-auth.dto.ts
やupdate-auth.dto.ts
は、一般的な CRUD 操作用のテンプレートです。認証システムでは、ログインや新規登録に特化した DTO が必要なため、認証専用の DTO を新しく作成します。これにより、入力データの厳密なバリデーションとセキュリティを確保できます。
注意: 自動生成された DTO ファイルは削除するか、以下の内容で上書きしてください。
認証用の DTO を作成します:
src/auth/dto/login.dto.ts (新規作成)
import { IsEmail, IsNotEmpty, IsString, MinLength } from "class-validator";
export class LoginDto {
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
@MinLength(6)
password: string;
}
src/auth/dto/register.dto.ts (新規作成)
import { IsEmail, IsNotEmpty, IsString, MinLength } from "class-validator";
import { UserStatus } from "@prisma/client";
export class RegisterDto {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
@MinLength(6)
password: string;
status?: UserStatus = UserStatus.FREE;
}
生成されたファイルの処理:
-
dto/create-auth.dto.ts
→ 削除または上記の RegisterDto で置き換え -
dto/update-auth.dto.ts
→ 削除(認証では通常、更新用 DTO は使用しない) -
entities/auth.entity.ts
→ 削除(Prisma を使用するため不要)
4. AuthService の実装
なぜ必要?
AuthService は認証の核となるビジネスロジックを実装します。ユーザー登録時のパスワードハッシュ化、ログイン時の認証処理、JWT トークンの生成など、セキュリティに関わる重要な処理を集約します。コントローラーからビジネスロジックを分離することで、テストしやすく保守性の高いコードになります。
src/auth/auth.service.ts
import {
Injectable,
UnauthorizedException,
ConflictException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { PrismaService } from "../prisma/prisma.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import * as bcrypt from "bcrypt";
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService, private jwtService: JwtService) {}
async register(registerDto: RegisterDto) {
const { name, email, password, status } = registerDto;
// メールアドレスの重複チェック
const existingUser = await this.prisma.user.findUnique({
where: { email },
});
if (existingUser) {
throw new ConflictException("このメールアドレスは既に使用されています");
}
// パスワードのハッシュ化
const hashedPassword = await bcrypt.hash(password, 10);
// ユーザー作成
const user = await this.prisma.user.create({
data: {
name,
email,
password: hashedPassword,
status: status || "FREE",
},
});
// パスワードを除外してレスポンス
const { password: _, ...result } = user;
return result;
}
async login(loginDto: LoginDto) {
const { email, password } = loginDto;
// ユーザー検索
const user = await this.prisma.user.findUnique({
where: { email },
});
if (!user) {
throw new UnauthorizedException(
"メールアドレスまたはパスワードが間違っています"
);
}
// パスワード検証
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException(
"メールアドレスまたはパスワードが間違っています"
);
}
// JWTトークン生成
const payload = { sub: user.id, email: user.email };
const accessToken = this.jwtService.sign(payload);
return {
accessToken,
user: {
id: user.id,
name: user.name,
email: user.email,
status: user.status,
},
};
}
async validateUser(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new UnauthorizedException("ユーザーが見つかりません");
}
const { password: _, ...result } = user;
return result;
}
}
5. JWT 戦略の実装
なぜ必要?
JWT 戦略は、受信した JWT トークンを検証し、認証されたユーザーの情報を取得するための Passport 戦略です。この戦略により、各リクエストでトークンの有効性を自動的に検証し、認証が必要なエンドポイントへのアクセス制御を実現します。
src/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { AuthService } from "../auth.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
const user = await this.authService.validateUser(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
6. JWT ガードの作成
なぜ必要?
ガードは、特定のエンドポイントに認証が必要かどうかを判断するためのメカニズムです。JWT ガードを作成することで、@UseGuards(JwtAuthGuard)
デコレータを使って、簡単にエンドポイントを保護できます。これにより、認証ロジックをコントローラーの各メソッドに書く必要がなくなります。
src/auth/guards/jwt-auth.guard.ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}
7. AuthController の実装
なぜ必要?
コントローラーは、HTTP リクエストを受け取り、適切なサービスメソッドを呼び出し、レスポンスを返す役割を担います。認証システムでは、ユーザー登録、ログイン、プロフィール取得などのエンドポイントを提供し、フロントエンドや API クライアントとの通信インターフェースとして機能します。
src/auth/auth.controller.ts
import {
Controller,
Post,
Body,
Get,
UseGuards,
Request,
} from "@nestjs/common";
import { AuthService } from "./auth.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import { JwtAuthGuard } from "./guards/jwt-auth.guard";
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post("register")
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@Post("login")
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@UseGuards(JwtAuthGuard)
@Get("profile")
getProfile(@Request() req) {
return req.user;
}
}
8. AuthModule の設定
なぜ必要?
Nest.js はモジュールベースのアーキテクチャを採用しています。AuthModule は認証関連のコンポーネント(サービス、コントローラー、戦略など)を統合し、他のモジュールから利用できるように設定します。また、JWT の設定(秘密鍵、有効期限など)もここで定義します。
src/auth/auth.module.ts
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { JwtStrategy } from "./strategies/jwt.strategy";
import { PrismaModule } from "../prisma/prisma.module";
@Module({
imports: [
PrismaModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: "24h" },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
9. App Module への追加
なぜ必要?
作成した AuthModule をアプリケーション全体で利用できるようにするため、ルートモジュール(AppModule)にインポートします。これにより、認証機能が他のモジュールからアクセス可能になり、アプリケーション全体で認証機能を使用できるようになります。
src/app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AuthModule } from "./auth/auth.module";
import { PrismaModule } from "./prisma/prisma.module";
// 他のモジュール...
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
PrismaModule,
AuthModule,
// 他のモジュール...
],
})
export class AppModule {}
10. 環境変数の設定
なぜ必要?
JWT 秘密鍵やデータベース接続情報などの機密情報は、コードに直接記述せず環境変数として管理する必要があります。これにより、異なる環境(開発、ステージング、本番)で異なる設定を使用でき、セキュリティも向上します。特に JWT_SECRET は、トークンの署名に使用される重要な秘密鍵です。
.env
DATABASE_URL="postgresql://username:password@localhost:5432/fleamarket"
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
11. 他のリソースでの認証利用
なぜ必要?
認証システムを構築した目的は、他の API エンドポイントで認証を要求することです。この章では、実際に ItemController で認証を使用する例を示し、ユーザー固有のデータへのアクセス制御や、認証されたユーザーの情報を使ったビジネスロジックの実装方法を学びます。
既存の ItemController で認証を使用する例:
src/item/item.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
Request,
} from "@nestjs/common";
import { ItemService } from "./item.service";
import { CreateItemDto } from "./dto/create-item.dto";
import { UpdateItemDto } from "./dto/update-item.dto";
import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
@Controller("items")
export class ItemController {
constructor(private readonly itemService: ItemService) {}
@UseGuards(JwtAuthGuard)
@Post()
create(@Body() createItemDto: CreateItemDto, @Request() req) {
return this.itemService.create(createItemDto, req.user.id);
}
@Get()
findAll() {
return this.itemService.findAll();
}
@UseGuards(JwtAuthGuard)
@Get("my-items")
findMyItems(@Request() req) {
return this.itemService.findByUserId(req.user.id);
}
// 他のエンドポイント...
}
12. API テスト
なぜ必要?
実装した認証システムが正しく動作するかを確認するため、実際の HTTP リクエストを使ってテストを行います。cURL コマンドを使った具体的なテスト例を示すことで、フロントエンド開発者や API 利用者が認証システムの使い方を理解できます。また、開発中のデバッグにも活用できます。
ユーザー登録
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "テストユーザー",
"email": "test@example.com",
"password": "password123"
}'
ログイン
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123"
}'
認証が必要なエンドポイントのアクセス
curl -X GET http://localhost:3000/auth/profile \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
まとめ
この実装により、以下の機能が提供されます:
- ユーザー登録(パスワードハッシュ化)
- ログイン(JWT トークン発行)
- 認証が必要なエンドポイントの保護
- ユーザー情報の取得
JWT 認証は、ステートレスで拡張性があり、マイクロサービスアーキテクチャにも適しています。本番環境では、JWT_SECRET をより安全な値に変更し、HTTPS 通信を使用することを推奨します。