Contents
環境構築と NestJS プロジェクトの作成
このセクションでは、Node.js と npm のバージョン確認から Nest CLI によるプロジェクト生成までを順番に示します。正しいバージョンが揃っていれば数分で開発環境が完成し、そのまま次の設定フェーズへ移行できます。
|
1 2 3 4 5 6 7 8 9 10 |
# 1️⃣ Node と npm のバージョン確認(Node ≥ 18、npm ≥ 9 が推奨) node -v # e.g. v20.5.0 npm -v # e.g. 10.2.3 # 2️⃣ Nest CLI をグローバルインストール npm i -g @nestjs/cli # 3️⃣ プロジェクト作成(npm パッケージマネージャ使用) nest new nest-graphql-auth --package-manager npm |
Tip
nest newコマンドは最新のテンプレートを自動で取得するため、手動設定ミスが起きにくいです。生成後は Git リポジトリへコミットしておくと、サンプルコードとの比較が容易になります。
GraphQL モジュール設定(Code First と Schema First の選択)
NestJS では 2 種類のスキーマ定義手法 が提供されます。以下でそれぞれの概要と実装例を示すので、プロジェクト要件に合わせて最適な方式を選んでください。
Code First アプローチでのスキーマ自動生成
Code First は TypeScript クラスから GraphQL スキーマを自動生成します。型安全性が高く、IDE の補完機能も活用できる点がメリットです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/users/user.model.ts import { Field, ObjectType, ID } from '@nestjs/graphql'; @ObjectType() export class User { @Field(() => ID) id: string; @Field() email: string; @Field({ nullable: true }) role?: string; } |
|
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 |
// src/app.module.ts import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { join } from 'path'; // ← 追加インポート @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, /** * autoSchemaFile に true を渡すと、アプリ起動時に * プロジェクトのルートディレクトリ(process.cwd())配下に * `schema.gql` が生成されます。 * * 生成先を明示したい場合は文字列でパスを指定します。 * autoSchemaFile: join(process.cwd(), 'src/schema.gql') */ autoSchemaFile: true, sortSchema: true, }), // 他のモジュール … ], }) export class AppModule {} |
ポイント
autoSchemaFile: trueは「一時的なファイルに出力」ではなく、デフォルトでプロジェクトルートにschema.gqlを作成します。明示的にパスを指定すれば任意のディレクトリへ出力でき、CI 環境でも差分管理がしやすくなります。
Schema First アプローチで SDL(Schema Definition Language)を手書き
既存の GraphQL スキーマがある場合やフロントエンドチームとスキーマ駆動開発を行う際に有効です。SDL ファイルから TypeScript の型定義も自動生成できます。
|
1 2 3 4 5 6 7 8 9 10 11 |
# src/graphql/user.graphql type User { id: ID! email: String! role: String } type Query { me: User } |
|
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 |
// src/app.module.ts import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { join } from 'path'; @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, /** * typePaths に *.graphql を列挙すると、指定された * パス配下のすべての SDL が自動的に読み込まれます。 */ typePaths: ['./src/**/*.graphql'], definitions: { // GraphQL スキーマから生成した TypeScript 定義ファイルの出力先 path: join(process.cwd(), 'src/graphql.ts'), }, }), // 他のモジュール … ], }) export class AppModule {} |
ポイント
typePathsとdefinitions.pathを併用することで、SDL の変更が即座に型安全なコードへ反映されます。
認証基盤の構築:AuthModule と JwtModule 設定、UserService のモック実装
この章では JWT 発行・検証ロジック を AuthModule に集約し、ユーザー検索はインメモリの UserService で代用します。実運用時には DB 接続に差し替えるだけで利用できます。
JwtModule のシークレットと有効期限設定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 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 { JwtStrategy } from './jwt.strategy'; import { UserService } from '../user/user.service'; @Module({ imports: [ PassportModule, JwtModule.register({ secret: process.env.JWT_SECRET ?? 'change_this_secret', signOptions: { expiresIn: '15m' }, // アクセストークン有効期限 }), ], providers: [AuthService, JwtStrategy, UserService], exports: [AuthService], }) export class AuthModule {} |
ダミーユーザーを保持する UserService
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/user/user.service.ts import { Injectable } from '@nestjs/common'; import { User } from './user.model'; @Injectable() export class UserService { // 本番では DB に置き換える想定のインメモリデータ private readonly users: User[] = [ { id: '1', email: 'alice@example.com', role: 'admin' }, { id: '2', email: 'bob@example.com', role: 'user' }, ]; async findByEmail(email: string): Promise<User | undefined> { return this.users.find(u => u.email === email); } async findById(id: string): Promise<User | undefined> { return this.users.find(u => u.id === id); } } |
ポイント
AuthServiceがUserServiceを呼び出すだけで認証フローが完結します。DB に差し替える際はUserServiceの実装だけを書き換えれば他のコードは変更不要です。
Passport と JWT ストラテジーの統合
Passport は NestJS が提供する Guard 機構と組み合わせることで、リクエストレベルで認証結果を request.user に自動注入します。以下に実装上の重要ポイントを示します。
JwtStrategy の実装
|
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 |
// src/auth/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 readonly userService: UserService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: process.env.JWT_SECRET ?? 'change_this_secret', }); } /** * validate は Passport が呼び出すフックです。 * 戻り値は自動的に request.user に設定されます。 */ async validate(payload: { sub: string }): Promise<any> { const user = await this.userService.findById(payload.sub); if (!user) { throw new UnauthorizedException('Invalid token'); } return user; // ← request.user に格納 } } |
認証が必要な GraphQL Resolver の書き方
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/user/user.resolver.ts import { Resolver, Query, Context } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; import { GqlAuthGuard } from '../auth/gql-auth.guard'; import { User } from './user.model'; @Resolver(() => User) export class UserResolver { @Query(() => User, { name: 'me' }) @UseGuards(GqlAuthGuard) // ← Guard が認証を実行 async getCurrentUser(@Context() ctx): Promise<User> { /** * GqlAuthGuard が JwtStrategy の結果(user オブジェクト)を * GraphQL コンテキストの `ctx.user` に設定します。 */ return ctx.user; } } |
ポイント
従来のreq.userに依存した実装から、GraphQL 用に統一されたctx.userへ置き換えることで、HTTP と GraphQL の両方で同じ Guard ロジックを再利用できます。
GraphQL コンテキストへの認証情報注入と GqlAuthGuard/RolesGuard
Apollo Server が提供する context 関数で JWT を解析し、GraphQL の実行コンテキストにユーザー情報を埋め込む方法を解説します。続いてロールベースの認可ガードまで実装します。
Apollo Server の context でトークン抽出
|
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 |
// src/app.module.ts(抜粋) import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { AuthModule } from './auth/auth.module'; import { GqlAuthGuard } from './auth/gql-auth.guard'; @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: true, /** * context 関数は各リクエストごとに呼び出されます。 * Authorization ヘッダーからトークンを取得し、後続の Guard が * 参照できるように `token` プロパティとして返します。 */ context: ({ req }) => { const authHeader = req.headers.authorization ?? ''; const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null; return { token }; }, }), AuthModule, // 他モジュール … ], }) export class AppModule {} |
GqlAuthGuard:Passport の JWT Strategy を GraphQL に橋渡し
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// src/auth/gql-auth.guard.ts import { ExecutionContext, Injectable } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class GqlAuthGuard extends AuthGuard('jwt') { /** * GraphQL のコンテキストに格納された token を * 標準的な HTTP ヘッダー形式へ変換して Passport に渡す。 */ getRequest(context: ExecutionContext) { const gqlCtx = GqlExecutionContext.create(context); const { token } = gqlCtx.getContext(); return { headers: { authorization: token ? `Bearer ${token}` : '', }, }; } } |
ロールベース認可のための Roles デコレータと Guard
|
1 2 3 4 5 |
// src/auth/roles.decorator.ts import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); |
|
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 |
// src/auth/roles.guard.ts import { CanActivate, ExecutionContext, Injectable, ForbiddenException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { GqlExecutionContext } from '@nestjs/graphql'; import { ROLES_KEY } from './roles.decorator'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>( ROLES_KEY, [context.getHandler(), context.getClass()], ); if (!requiredRoles?.length) return true; // ロール指定がなければ許可 const gqlCtx = GqlExecutionContext.create(context); const user = gqlCtx.getContext().user; if (!user || !requiredRoles.includes(user.role)) { throw new ForbiddenException('Insufficient role'); } return true; } } |
Guard の組み合わせ例(管理者専用クエリ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/user/admin.resolver.ts import { Resolver, Query } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; import { GqlAuthGuard } from '../auth/gql-auth.guard'; import { RolesGuard } from '../auth/roles.guard'; import { Roles } from '../auth/roles.decorator'; import { User } from './user.model'; import { UserService } from './user.service'; @Resolver(() => User) export class AdminResolver { constructor(private readonly userService: UserService) {} @Query(() => [User], { name: 'allUsers' }) @UseGuards(GqlAuthGuard, RolesGuard) @Roles('admin') async getAllUsers() { return this.userService.findAll(); } } |
ポイント
GqlAuthGuardが JWT を検証し、結果のユーザーオブジェクトは GraphQL コンテキスト (ctx.user) に格納されます。RolesGuardはその情報を元にロールチェックを行うので、認可ロジックが一貫して動作します。
Refresh Token の実装概要と e2e テスト
アクセストークンは短時間で期限切れになるため、リフレッシュトークン を別途管理し、無駄な再ログインを防ぎます。以下では HttpOnly Cookie に保存する安全な設計と、実際に動作させるエンドポイント・テストコードを示します。
Refresh Token を HttpOnly Cookie に格納するサービスメソッド
|
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 |
// src/auth/auth.service.ts(抜粋) import { Injectable, Response } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { User } from '../user/user.model'; @Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, @InjectResponse() private readonly response: Response, // カスタムデコレータ例 ) {} async login(user: User) { const payload = { sub: user.id, role: user.role }; const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' }); const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' }); // HttpOnly Cookie に保存(Express の例) this.response.cookie('refresh_token', refreshToken, { httpOnly: true, sameSite: 'strict', secure: process.env.NODE_ENV === 'production', maxAge: 7 * 24 * 60 * 60 * 1000, // 7日 }); return { accessToken }; } /** Refresh Token の検証と新規アクセストークン発行 */ async refresh(refreshToken: string) { const payload = this.jwtService.verify(refreshToken); return this.jwtService.sign({ sub: payload.sub, role: payload.role }, { expiresIn: '15m' }); } } |
GraphQL Resolver におけるリフレッシュエンドポイント
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/auth/auth.resolver.ts import { Resolver, Mutation, Context } from '@nestjs/graphql'; import { UnauthorizedException } from '@nestjs/common'; @Resolver() export class AuthResolver { constructor(private readonly authService: AuthService) {} @Mutation(() => String, { name: 'refreshAccessToken' }) async refresh(@Context('req') req, @Context('res') res): Promise<string> { const token = req.cookies?.refresh_token; if (!token) throw new UnauthorizedException('No refresh token'); // 新しいアクセストークンを生成し、必要なら新しいリフレッシュトークンも再設定 return this.authService.refresh(token); } } |
e2e テストで認証フロー全体を検証
|
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 |
// test/e2e/auth.e2e-spec.ts import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; import { INestApplication } from '@nestjs/common'; import { AppModule } from '../../src/app.module'; describe('Auth Flow (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('login → access token + refresh cookie', async () => { const res = await request(app.getHttpServer()) .post('/graphql') .send({ query: `mutation { login(email:"alice@example.com") { accessToken } }` }) .expect(200); expect(res.body.data.login.accessToken).toBeDefined(); expect(res.headers['set-cookie'][0]).toContain('refresh_token'); }); it('refresh → new access token', async () => { // まず login して cookie を取得 const loginRes = await request(app.getHttpServer()) .post('/graphql') .send({ query: `mutation { login(email:"alice@example.com") { accessToken } }` }); const cookies = loginRes.headers['set-cookie']; const refreshRes = await request(app.getHttpServer()) .post('/graphql') .set('Cookie', cookies) .send({ query: `{ __typename }` }) // 任意のクエリでミドルウェアを通す .expect(200); expect(refreshRes.body.data.refreshAccessToken).toBeDefined(); }); afterAll(async () => { await app.close(); }); }); |
ポイント
- テストは GraphQL エンドポイント (/graphql) に対して HTTP リクエストを送信し、Cookie の受け渡しまで検証します。
- CI 環境でも同様に実行できるので、認証ロジックの回帰テストが容易になります。
まとめ
- 環境構築:Node ≥ 18 と Nest CLI を用いれば数分で開発基盤が完成します。
- GraphQL 設定:Code First/Schema First のどちらも
@nestjs/graphqlv12 でシームレスに利用可能です。autoSchemaFileはデフォルトでプロジェクトルートにschema.gqlを生成し、パスを明示すれば任意の場所へ出力できます。 - 認証基盤:
AuthModuleにJwtModuleとシンプルなインメモリUserServiceを組み込み、JwtStrategyがトークン検証とユーザー取得を担います。 - Guard の統合:
GqlAuthGuardが GraphQL コンテキストにuserを注入し、Resolver はctx.userで取得できるため HTTP と GraphQL の認証ロジックが統一されます。 - ロールベース制御:
RolesデコレータとRolesGuardにより、細かい権限チェックを簡潔に実装できます。 - Refresh Token:HttpOnly Cookie で安全に保存し、リフレッシュエンドポイントと e2e テストで全体フローを検証可能です。
以上の手順通りに構築すれば、NestJS と GraphQL の組み合わせで スケーラブルかつテスト容易な JWT 認証システム が完成します。サンプルコードは GitHub のリポジトリでも公開中ですので、ぜひクローンして動作を確認し、自プロジェクトに合わせてカスタマイズしてください。