Open1

Next.jsのServer ActionsでAWS Cognitoを操作する

mizukichmizukich

Next.jsの最新機能であるServer Actionsと、AWSの認証サービスであるCognitoを組み合わせることで、サーバーサイドで安全にユーザー管理を行えるようになるそう。

Cognitoとは何か?

AWS Cognito(コグニート)は、Webアプリケーションやモバイルアプリのユーザー認証と認可を管理するためのAWSのサービスです。

Cognitoの主な機能

  • ユーザープール: ユーザーディレクトリを作成・管理する機能
  • IDプール: 一時的なAWS認証情報を発行する機能
  • ソーシャルサインイン: Google、Facebook、Amazonなどのソーシャルアカウントでのログイン
  • MFA(多要素認証): セキュリティ強化のための2段階認証

Cognitoの基本概念

  1. ユーザープール「ユーザーのデータベース」メールアドレスやパスワードなどのユーザー情報を保存・管理

  2. アプリクライアントはアプリケーションがCognitoのAPIを呼び出すために必要な識別子

  3. ユーザー属性はユーザーに関連付けられた情報(名前、メールアドレス、電話番号など)

Server Actionsとは何か?

Server Actionsは、Next.js 13から導入された機能で、クライアントコンポーネントからサーバー上の関数を直接呼び出すことができる。フォームの送信処理やデータ取得などをサーバーサイドで実行できるため、セキュリティとパフォーマンスが向上

Server Actionsの特徴

  • クライアントコンポーネントからサーバー関数を直接呼び出せる
  • 'use server' ディレクティブでサーバーコードを明示的に宣言
  • フォームの送信処理などをクライアントJavaScriptなしで実行可能
  • クライアントに公開したくない処理やAPIキーを安全に扱える

基本的な書き方(メモ)

'use server'

// この関数はサーバーサイドでのみ実行される
export async function myServerAction(formData: FormData) {
  // サーバーサイドの処理
  const name = formData.get('name')
  
  // データベース操作やAPIリクエストなど
  
  // 結果を返す
  return { success: true, message: `Hello, ${name}!` }
}

Cognitoユーザープールを操作する

AWS Cognitoユーザープールを操作するには、Server Actionsを使うことで安全にAPIキーを扱いながら実装できます。以下、主要な操作方法を解説します。

1. AWS SDKのセットアップ

まず、必要なパッケージをインストールします:

npm install @aws-sdk/client-cognito-identity-provider

2. ユーザー一覧を取得する

'use server'

import {
  CognitoIdentityProviderClient,
  ListUsersCommand
} from '@aws-sdk/client-cognito-identity-provider'

export type UserInfo = {
  email: string
  status: string
}

export async function listUsers(): Promise<UserInfo[]> {
  const region = process.env.AUTH_COGNITO_REGION ?? 'ap-northeast-1'
  const userPoolId = process.env.AUTH_COGNITO_USER_POOL_ID ?? ''
  
  if (!userPoolId) {
    throw new Error('ユーザープールIDが設定されていません')
  }
  
  try {
    // Cognitoクライアントの初期化
    const cognitoClient = new CognitoIdentityProviderClient({ region })
    
    // ユーザー一覧取得コマンドの作成
    const command = new ListUsersCommand({
      UserPoolId: userPoolId,
      AttributesToGet: ['email']
    })
    
    // コマンドの実行
    const response = await cognitoClient.send(command)
    
    // レスポンスからユーザー情報を抽出
    return (response.Users || []).map(user => {
      // メールアドレスを取得
      const emailAttribute = user.Attributes?.find(
        attr => attr.Name === 'email'
      )
      
      return {
        email: emailAttribute?.Value || user.Username || '',
        status: user.UserStatus || ''
      }
    })
  } catch (error) {
    console.error('ユーザー一覧取得エラー:', error)
    throw new Error(
      'ユーザー一覧の取得に失敗しました: ' +
      (error instanceof Error ? error.message : String(error))
    )
  }
}

3. 新規ユーザーを登録する

'use server'

import {
  CognitoIdentityProviderClient,
  AdminCreateUserCommand
} from '@aws-sdk/client-cognito-identity-provider'

export async function registerUser(email: string): Promise<void> {
  const region = process.env.AUTH_COGNITO_REGION ?? 'ap-northeast-1'
  const userPoolId = process.env.AUTH_COGNITO_USER_POOL_ID ?? ''
  
  if (!userPoolId) {
    throw new Error('ユーザープールIDが設定されていません')
  }
  
  try {
    // Cognitoクライアントの初期化
    const cognitoClient = new CognitoIdentityProviderClient({ region })
    
    // ユーザー作成コマンドの作成
    const command = new AdminCreateUserCommand({
      UserPoolId: userPoolId,
      Username: email,
      UserAttributes: [
        { Name: 'email', Value: email },
        { Name: 'email_verified', Value: 'true' }
      ],
      DesiredDeliveryMediums: ['EMAIL'] // 仮パスワードをメールで送信
    })
    
    // コマンドの実行
    await cognitoClient.send(command)
  } catch (error) {
    console.error('ユーザー登録エラー:', error)
    
    // エラーハンドリング
    if (error instanceof Error) {
      // ユーザーが既に存在する場合
      if (error.name === 'UsernameExistsException') {
        throw new Error(`ユーザー「${email}」は既に存在します`)
      }
    }
    
    throw new Error(
      'ユーザー登録に失敗しました: ' +
      (error instanceof Error ? error.message : String(error))
    )
  }
}

フロントエンドとの連携

これらのServer Actionsをフロントエンド(クライアントコンポーネント)から呼び出す方法を見ていきましょう。

ユーザー一覧を表示するコンポーネント

'use client'

import { useState, useEffect } from 'react'
import { listUsers } from '@/app/actions/user-actions'
import type { UserInfo } from '@/app/actions/user-actions'

export function UserList() {
  const [users, setUsers] = useState<UserInfo[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  
  useEffect(() => {
    async function fetchUsers() {
      try {
        const data = await listUsers()
        setUsers(data)
      } catch (err) {
        setError(err instanceof Error ? err.message : '不明なエラーが発生しました')
      } finally {
        setLoading(false)
      }
    }
    
    fetchUsers()
  }, [])
  
  if (loading) return <div>読み込み中...</div>
  if (error) return <div>エラー: {error}</div>
  
  return (
    <div>
      <h2>ユーザー一覧</h2>
      <table>
        <thead>
          <tr>
            <th>メールアドレス</th>
            <th>ステータス</th>
          </tr>
        </thead>
        <tbody>
          {users.map(user => (
            <tr key={user.email}>
              <td>{user.email}</td>
              <td>{user.status}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

ユーザー登録フォームコンポーネント

'use client'

import { useState } from 'react'
import { registerUser } from '@/app/actions/user-actions'

export function UserRegistrationForm() {
  const [email, setEmail] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [success, setSuccess] = useState(false)
  
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setLoading(true)
    setError(null)
    setSuccess(false)
    
    try {
      await registerUser(email)
      setSuccess(true)
      setEmail('')
    } catch (err) {
      setError(err instanceof Error ? err.message : '不明なエラーが発生しました')
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <div>
      <h2>新規ユーザー登録</h2>
      {success && <div className="success">ユーザーを登録しました。確認メールを送信しました。</div>}
      {error && <div className="error">{error}</div>}
      
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">メールアドレス:</label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={e => setEmail(e.target.value)}
            required
          />
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? '登録中...' : 'ユーザーを登録'}
        </button>
      </form>
    </div>
  )
}

環境設定

AWS Cognitoを使うには、適切な環境変数を設定する必要がある

# Cognito設定
AUTH_COGNITO_REGION=ap-northeast-1
AUTH_COGNITO_USER_POOL_ID=ap-northeast-1_xxxxxxxxx
AUTH_COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxx

# AWS認証情報(開発環境用)
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

認証情報の取得方法

Server ActionsでCognitoを操作するためには、AWS SDKが認証情報を取得できる必要があります。AWS SDKは以下の順序で認証情報を探す:

  1. 環境変数(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
  2. 共有認証情報ファイル(~/.aws/credentials)
  3. ECS環境変数(Amazon ECS)
  4. インスタンスメタデータサービス(EC2インスタンス)

参考リソース