NestJSでAuth0 Management APIを使ったユーザー登録

Auth0のManagement APIとNestJSを使って、プログラムからAuth0にユーザーを登録する方法を解説します。REST APIとGraphQL両方の実装例を紹介します。

#nestjs #auth0 #user-management

Auth0 Management APIとは

Auth0 Management APIは、Auth0のリソース(ユーザー、アプリケーション、接続など)をプログラムから操作するためのREST APIです。

主な用途

  • ユーザーの作成・更新・削除
  • ロールやパーミッションの管理
  • ログの取得と分析
  • アプリケーション設定の管理

事前準備

1. パッケージのインストール

npm install auth0
npm install -D @types/auth0

2. Auth0でアプリケーションを作成

Auth0ダッシュボードでMachine to Machine Applicationを作成し、以下の情報を取得します:

  • Domain: your-tenant.auth0.com
  • Client ID: アプリケーションID
  • Client Secret: シークレットキー

3. 必要なスコープを付与

Management APIに対して以下のスコープを付与します:

  • create:users: ユーザーの作成
  • read:users: ユーザー情報の取得
  • update:users: ユーザー情報の更新
  • delete:users: ユーザーの削除(必要に応じて)

NestJSでの実装

環境変数の設定

// config/configuration.ts
export default () => ({
  auth0: {
    domain: process.env.AUTH0_DOMAIN,
    clientId: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
  },
})
# .env
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=your_client_id
AUTH0_CLIENT_SECRET=your_client_secret

Auth0モジュールの実装

Module

import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { Auth0Service } from './auth0.service'

@Module({
  imports: [ConfigModule],
  providers: [Auth0Service],
  exports: [Auth0Service],
})
export class Auth0Module {}

Service

import { Injectable, InternalServerErrorException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { ManagementClient, User } from 'auth0'

export interface CreateUserInput {
  email: string
  password: string
  name?: string
}

export interface CreateUserOutput {
  userId: string
  email: string
}

@Injectable()
export class Auth0Service {
  private readonly client: ManagementClient
  private readonly connection = 'Username-Password-Authentication'

  constructor(private configService: ConfigService) {
    this.client = new ManagementClient({
      domain: this.configService.get<string>('auth0.domain')!,
      clientId: this.configService.get<string>('auth0.clientId')!,
      clientSecret: this.configService.get<string>('auth0.clientSecret')!,
      scope: 'create:users read:users update:users',
    })
  }

  async createUser(input: CreateUserInput): Promise<CreateUserOutput> {
    try {
      const user = await this.client.createUser({
        connection: this.connection,
        email: input.email,
        password: input.password,
        name: input.name,
        email_verified: false,
      })

      if (!user.user_id) {
        throw new Error('User ID not returned from Auth0')
      }

      return {
        userId: user.user_id,
        email: user.email!,
      }
    } catch (error) {
      this.handleAuth0Error(error)
    }
  }

  async getUserById(userId: string): Promise<User> {
    try {
      return await this.client.getUser({ id: userId })
    } catch (error) {
      this.handleAuth0Error(error)
    }
  }

  async updateUser(userId: string, data: Partial<User>): Promise<User> {
    try {
      return await this.client.updateUser({ id: userId }, data)
    } catch (error) {
      this.handleAuth0Error(error)
    }
  }

  async deleteUser(userId: string): Promise<void> {
    try {
      await this.client.deleteUser({ id: userId })
    } catch (error) {
      this.handleAuth0Error(error)
    }
  }

  private handleAuth0Error(error: any): never {
    if (error.statusCode === 409) {
      throw new InternalServerErrorException('User already exists')
    }

    if (error.statusCode === 400) {
      throw new InternalServerErrorException(
        `Invalid input: ${error.message}`,
      )
    }

    throw new InternalServerErrorException(
      `Auth0 error: ${error.message || 'Unknown error'}`,
    )
  }
}

REST APIでの実装例

Controller

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common'
import { Auth0Service, CreateUserInput } from './auth0.service'

export class CreateUserDto {
  email: string
  password: string
  name?: string
}

export class UpdateUserDto {
  name?: string
  email?: string
}

@Controller('users')
export class UsersController {
  constructor(private readonly auth0Service: Auth0Service) {}

  @Post()
  async createUser(@Body() dto: CreateUserDto) {
    return this.auth0Service.createUser(dto)
  }

  @Get(':userId')
  async getUser(@Param('userId') userId: string) {
    return this.auth0Service.getUserById(userId)
  }

  @Put(':userId')
  async updateUser(
    @Param('userId') userId: string,
    @Body() dto: UpdateUserDto,
  ) {
    return this.auth0Service.updateUser(userId, dto)
  }

  @Delete(':userId')
  async deleteUser(@Param('userId') userId: string) {
    await this.auth0Service.deleteUser(userId)
    return { message: 'User deleted successfully' }
  }
}

DTOのバリデーション

import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'

export class CreateUserDto {
  @IsEmail()
  email: string

  @IsString()
  @MinLength(8)
  password: string

  @IsString()
  @IsOptional()
  name?: string
}
npm install class-validator class-transformer

GraphQL APIでの実装例

Types

import { Field, InputType, ObjectType } from '@nestjs/graphql'

@InputType()
export class CreateUserInput {
  @Field()
  email: string

  @Field()
  password: string

  @Field({ nullable: true })
  name?: string
}

@ObjectType()
export class CreateUserOutput {
  @Field()
  userId: string

  @Field()
  email: string
}

Resolver

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
import { Auth0Service } from '../auth0/auth0.service'
import { CreateUserInput, CreateUserOutput } from './user.types'

@Resolver()
export class UsersResolver {
  constructor(private readonly auth0Service: Auth0Service) {}

  @Mutation(() => CreateUserOutput)
  async createUser(
    @Args('input') input: CreateUserInput,
  ): Promise<CreateUserOutput> {
    return this.auth0Service.createUser(input)
  }

  @Query(() => String)
  async getUserById(@Args('userId') userId: string) {
    const user = await this.auth0Service.getUserById(userId)
    return JSON.stringify(user)
  }
}

GraphQLクエリ例

# ユーザー作成
mutation CreateUser {
  createUser(
    input: {
      email: "user@example.com"
      password: "SecurePassword123!"
      name: "John Doe"
    }
  ) {
    userId
    email
  }
}

# ユーザー取得
query GetUser {
  getUserById(userId: "auth0|123456789")
}

セキュリティのベストプラクティス

1. パスワードポリシー

Auth0側でパスワードポリシーを設定します:

  • 最小8文字以上
  • 大文字・小文字・数字・記号を含む
  • 一般的なパスワードを禁止

2. 環境変数の管理

// ❌ ハードコード(危険)
const client = new ManagementClient({
  domain: 'your-tenant.auth0.com',
  clientId: 'hardcoded-id',
  clientSecret: 'hardcoded-secret',
})

// ✅ 環境変数で管理
const client = new ManagementClient({
  domain: process.env.AUTH0_DOMAIN!,
  clientId: process.env.AUTH0_CLIENT_ID!,
  clientSecret: process.env.AUTH0_CLIENT_SECRET!,
})

3. エラーハンドリング

try {
  await this.auth0Service.createUser(input)
} catch (error) {
  if (error.message.includes('already exists')) {
    // ユーザーが既に存在する場合の処理
    throw new ConflictException('User already exists')
  }
  throw error
}

4. レート制限

Auth0 Management APIにはレート制限があります:

import { Throttle } from '@nestjs/throttler'

@Controller('users')
export class UsersController {
  @Throttle(10, 60) // 60秒間に10リクエストまで
  @Post()
  async createUser(@Body() dto: CreateUserDto) {
    return this.auth0Service.createUser(dto)
  }
}

テストの実装

import { Test, TestingModule } from '@nestjs/testing'
import { ConfigService } from '@nestjs/config'
import { Auth0Service } from './auth0.service'

describe('Auth0Service', () => {
  let service: Auth0Service

  const mockConfigService = {
    get: jest.fn((key: string) => {
      const config = {
        'auth0.domain': 'test.auth0.com',
        'auth0.clientId': 'test-client-id',
        'auth0.clientSecret': 'test-secret',
      }
      return config[key]
    }),
  }

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        Auth0Service,
        {
          provide: ConfigService,
          useValue: mockConfigService,
        },
      ],
    }).compile()

    service = module.get<Auth0Service>(Auth0Service)
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })

  // 実際のAuth0 APIを呼ばないモックテストを実装
})

まとめ

Auth0 Management APIをNestJSで使うには:

  • 認証情報の管理: 環境変数で安全に管理
  • エラーハンドリング: 適切なエラー処理とユーザーフィードバック
  • セキュリティ: パスワードポリシーとレート制限を実装
  • バリデーション: DTOでの入力検証
  • テスト: モックを使った単体テスト

REST APIとGraphQL両方で実装できるため、プロジェクトに応じて選択できます。

参考リンク