NestJSとHasuraをAuth0で認証する実装ガイド

NestJSのAPIエンドポイントとHasuraのGraphQLをAuth0で統一的に認証する方法を解説します。Guardパターンを使った実装例とJWT検証の実践的なコード例を紹介します。

#graphql #hasura #nestjs #auth0

はじめに

NestJSのAPIエンドポイントとHasuraのGraphQLに対する認証基盤としてAuth0を採用した際の実装方法を解説します。

Auth0公式サイト

アーキテクチャ

  • Hasura: GraphQLエンドポイントを提供。シンプルなCRUD操作を担当
  • NestJS: Hasuraで吸収できない複雑なビジネスロジックを担当
  • Auth0: 両方のエンドポイントを統一的に認証

Hasuraの認証設定

基本設定

Hasuraの公式チュートリアルに従って設定を進めます。

Auth0公開鍵の設定

Docker環境の場合、Auth0から公開鍵を取得し、環境変数に設定する必要があります。

公開鍵の取得

公開鍵取得手順に従って公開鍵を発行します。

docker-compose設定

# docker-compose.yml
version: '3.6'
services:
  postgres:
    image: postgres
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data

  graphql-engine:
    image: hasura/graphql-engine:v1.0.0-beta.6
    ports:
      - "8080:8080"
    depends_on:
      - "postgres"
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:@postgres:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
      # HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
      HASURA_GRAPHQL_JWT_SECRET: '取得した公開鍵を設定'

volumes:
  db_data:

NestJSの認証実装

NestJS側のAPIエンドポイントを直接呼び出されないよう、Auth0で保護します。

環境変数の設定

# .env
# Auth0 Domain
AUTH0_ISSUER_URL="https://your-domain.auth0.com/"

# Auth0 Identifier
AUTH0_AUDIENCE="your-api-identifier"

本番環境では、.envファイルではなく、Cloud Run Secretsなどのシークレット管理サービスを使用することを推奨します。

AuthGuardの実装

NestJSのGuard機能を使って、リクエストの認証を検証します。

Guardの作成

nest g guard auth/auth-guard

Guardの実装

// src/common/guard/auth/auth-guard.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common'
import { GqlContextType } from '@nestjs/graphql'
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'
import { Reflector } from '@nestjs/core'
import { expressjwt, GetVerificationKey } from 'express-jwt'
import { expressJwtSecret } from 'jwks-rsa'
import { ConfigService } from '@nestjs/config'
import { promisify } from 'util'

@Injectable()
export class AuthGuard implements CanActivate {
  private readonly AUTH0_AUDIENCE: string
  private readonly AUTH0_ISSUER_URL: string

  constructor(
    @InjectPinoLogger(AuthGuard.name) private readonly logger: PinoLogger,
    private readonly reflector: Reflector,
    private readonly configService: ConfigService,
  ) {
    this.AUTH0_AUDIENCE = this.configService.get('AUTH0_AUDIENCE')
    this.AUTH0_ISSUER_URL = this.configService.get('AUTH0_ISSUER_URL')
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // GraphQLリクエストはスキップ(Hasura経由の場合)
    if (context.getType<GqlContextType>() === 'graphql') {
      return true
    }

    // Auth0に対してJWT Tokenの検証を実行
    const checkJwtToken = await promisify(
      expressjwt({
        secret: expressJwtSecret({
          cache: true,
          rateLimit: true,
          jwksRequestsPerMinute: 5,
          jwksUri: `${process.env.AUTH0_ISSUER_URL}.well-known/jwks.json`,
        }) as GetVerificationKey,
        audience: this.AUTH0_AUDIENCE,
        issuer: this.AUTH0_ISSUER_URL,
        algorithms: ['RS256'],
      }),
    )

    try {
      await checkJwtToken(
        context.switchToHttp().getRequest(),
        context.switchToHttp().getResponse(),
      )
      return true
    } catch (e: unknown) {
      throw new UnauthorizedException(e)
    }
  }
}

エンドポイントへの適用

// src/app.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common'
import { AppService } from './app.service'
import { AuthGuard } from './common/guard/auth/auth-guard.guard'

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello()
  }

  @UseGuards(AuthGuard)
  @Get('/private')
  async private() {
    return { message: '認証成功' }
  }
}

CORS設定

// src/main.ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { Logger } from 'nestjs-pino'
import { AllExceptionsFilter } from './common/filter/all-exceptions.filter'

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { bufferLogs: true })
  app.useLogger(app.get(Logger))

  const adapterHost = app.get(HttpAdapterHost)
  const httpAdapter = adapterHost.httpAdapter
  const instance = httpAdapter.getInstance()
  app.useGlobalFilters(new AllExceptionsFilter(instance))

  app.enableCors({
    origin: '*',
    allowedHeaders:
      'Origin, X-Requested-With, Content-Type, Accept, Authorization',
  })

  await app.listen(3000)
}
bootstrap()

動作確認

トークン取得スクリプト

Auth0からアクセストークンを取得します。

参考: Auth0 + NestJS バックエンドサンプル

#!/usr/bin/env bash

auth_url=https://your-domain.auth0.com
client_id=your_client_id
client_secret=your_client_secret
username="user@example.com"
password="password"

echo "🎁 id_tokenを取得中..."

curl -s --request POST \
  --url ${auth_url}/oauth/token \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=password \
  --data username=${username} \
  --data password=${password} \
  --data client_id=${client_id} \
  --data client_secret=${client_secret}

echo "\n"

APIテスト

取得したid_tokenを使ってAPIをテストします。

# 認証成功
curl -i -X GET 'http://localhost:3000/private' \
  -H 'Authorization: Bearer <id_token>'

# レスポンス例
# HTTP/1.1 200 OK
# Content-Type: application/json; charset=utf-8
#
# {"message":"認証成功"}

# 認証失敗(不正なトークン)
curl -i -X GET 'http://localhost:3000/private' \
  -H 'Authorization: Bearer invalid_token'

# レスポンス例
# HTTP/1.1 401 Unauthorized

まとめ

Auth0を使ってNestJSとHasuraの認証を統一することで、以下のメリットが得られます:

  • 一貫性: 単一の認証基盤でマイクロサービス全体を保護
  • セキュリティ: JWT検証により、改ざんされたトークンを検出
  • 柔軟性: Auth0の豊富な機能(MFA、ソーシャルログインなど)を活用

この実装パターンは、マイクロサービスアーキテクチャにおける認証のベストプラクティスの一つです。