Amplify Adapter Next.js と Cognito でログインをしてみた

Amplify Adapter Next.js と Cognito でログインをしてみた

Next.jsとAWS Cognitoを使った認証システムの実装方法を解説した記事です。複数存在するAmplifyライブラリの役割を整理し、ログイン機能の実装からユーザーグループによる権限管理まで、実践的なコード例とともに段階的に説明しています。エラーハンドリングの方法も含めた包括的な内容となっています。
2025.09.29

Next.js を利用した Cognito User Pools でのログインを実装してみました。
基本構成を覚えれば難しいところはないのですが、ライブラリが散見しているのでまとめました。これから実装を進めたい人の役に立てば何よりです。

要点のまとめ

いくつか知るべきことはありますが、要点を頭に入れておけばすんなりと進むかと思います。

  • aws-amplify/auth を利用して Client Components でログインを実装する
  • aws-amplify/auth/server で Server Components からユーザー情報にアクセスする
  • @aws-amplify/adapter-nextjs と Next.js の cookies で連携をする
  • @aws-amplify/ui-reactのプロバイダーでアプリをラップする
  • @aws-amplify/ui-react で Client Components からユーザー情報にアクセスする

ライブラリが少し多いので、具体的な使い分けがあやふやになってしまいます。
それぞれの用途を簡単にまとめましょう。

aws-amplify/auth

クライアントサイドでのログインを主に担当します。ユーザー情報を取得したりもできます。
Client Components のハンドラーで呼び出したり、Hooks で呼び出したりします。

@aws-amplify/ui-react

クライアントサイドでの簡単に使える UI や便利な Hooks を提供します。

aws-amplify/auth/server

サーバーサイドでのJWTの取得やユーザー情報の取得をおこないます。
なので Server Components や API Routes での呼び出しが主になります。

@aws-amplify/adapter-nextjs

Next.js で cookies ベースの認証情報管理を行うためのアダプターです。 aws-amplify/auth/server と組み合わせて利用します。

ここまでライブラリの目的を理解したところで、Cognito User Pools を利用したログインと、User Groups を利用した権限管理の実装をしてみましょう。

Amlify のライブラリを中心にログインを実装する

それではどのように実装したかについてまとめていきます。実行時の環境は下記の通りです。Next.js のブートストラップなどは省略します。

  • React: 19.x
  • Next.js: 15.5.x
  • aws-amplify: 6.15.x
  • TypeScript: 5.x
  • Node.js: 24.x

ディレクトリ構造

Page Components では、認証後に表示可能なページを authenticatedでまとめます。グルーピングすることで、認証が必要なページとそうではない部分の見分けがつきやすくレビューなどしやすくなります。また、bulletproof-react に近い考えで、機能の役割ごとにディレクトリを features にまとめます。今回は認証/認可/ログイン周りを features/auth にまとめています。実際はこれ以外にも機能があるので諸々追加されていく形にはなります...。

			
			├── app
│   ├── (authenticated)
│   │   └── page.tsx
│   ├── layout.tsx
│   └── login
│       └── page.tsx
├── features
│   ├── auth
│   │   ├── components
│   │   │   ├── confirm-login.tsx
│   │   │   └── login.tsx
│   │   ├── configs
│   │   │   └── index.ts
│   │   ├── hooks
│   │   │   └── index.ts
│   │   ├── models
│   │   │   └── index.ts
│   │   ├── providers
│   │   │   └── index.tsx
│   │   └── utils
│   │       └── server.ts

		

Amplify config を仕込む

まずは Amplify の設定を作成します。Cognito User Pool の ID関連を指定するだけです。
また環境を簡単に切り替えられるように、ID を環境変数で管理しています。
この設定ファイルはクライアントでもサーバーでも利用するので、NEXT_PUBLIC を接頭語につけています。

features/auth/configs/index.ts
			
			import type { ResourcesConfig } from 'aws-amplify'

export const amplifyConfig: ResourcesConfig = {
    Auth: {
        Cognito: {
            userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID ?? '',
            userPoolClientId: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID ?? '',
            loginWith: {
                email: true,
            },
        },
    },
}

		

Client Components で Amplify を使えるようにする

クライアントで Amify.configure(config) を呼び出します。また、@aws-amplify/ui-react の機能を使えるように Provider でアプリをラップします。なのでこの2つを実行する Component を作成します。

features/auth/providers/index.tsx
			
			'use client'

import { Authenticator } from '@aws-amplify/ui-react'
import { Amplify } from 'aws-amplify'
import type { ReactNode } from 'react'
import { amplifyConfig } from '../configs'

Amplify.configure(amplifyConfig, { ssr: true })

export const AmplifyProvider = ({ children }: { children: ReactNode }) => {
    return <Authenticator.Provider>{children}</Authenticator.Provider>
}


		

あとはこの Provider コンポーネントを Root Layout に渡してあげれば完了です。

app/layout.tsx
			
			import { AmplifyProvider } from '@/features/auth/providers'
import './globals.css'

export default function RootLayout({
	children,
}: Readonly<{
	children: React.ReactNode
}>) {
	return (
		<html lang="en">
			<AmplifyProvider>
				<body className="antialiase">{children}</body>
			</AmplifyProvider>
		</html>
	)
}


		

Server Components でユーザー情報の取得をする

最後に Server Components でユーザー情報にアクセスできるようにします。
createServerRunner で Amplify の設定を渡して、Next.js 上で動かせるようにします。
関数のレスポンスである runWithAmplifyServerContext には、cookies を渡してあげて、operation で実際の処理を記述します。

features/auth/utils/server.ts
			
			import { createServerRunner } from '@aws-amplify/adapter-nextjs'
import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth/server'
import { cookies } from 'next/headers'
import { cache } from 'react'
import { amplifyConfig } from '../configs'

const { runWithAmplifyServerContext } = createServerRunner({
	config: amplifyConfig,
})

export const getUser = cache(async () => {
	return runWithAmplifyServerContext({
		nextServerContext: {
			cookies,
		},
		operation: async (contextSpec) => {
			const [session, user] = await Promise.all([
				fetchAuthSession(contextSpec),
				getCurrentUser(contextSpec),
			])

			return {
				name: user.username,
				email: user.signInDetails?.loginId,
				groups: session.tokens?.idToken?.payload['cognito:groups'] ?? [],
			}
		},
	})
})

export const getJsonWebToken = cache(async () => {
	return runWithAmplifyServerContext({
		nextServerContext: {
			cookies,
		},
		operation: (contextSpec) => {
			return fetchAuthSession(contextSpec).then((item) =>
				item.tokens?.idToken?.toString(),
			)
		},
	})
})

		

とりあえず、ユーザー情報と ID Token を取得する処理を書いてみました。

ログイン機能を実装する

ここから、Client Components の実装をします。流れとしては下記の通りです。

  1. form で username と password を受け取る
  2. handler で signIn 関数を呼び出す
  3. signIn 関数の signInStep がある場合にそれに応じた処理をする

FormData を handler で扱うだけなので処理は単純です。
signIn 関数の result.nextStep.signInStep というサインインプロセスの各段階を表す列挙型の処理が煩雑になります。

まず、ログインするためのコンポーネントを作成します。handler を受け取って、form 要素に渡すだけです。本当はスタイリングなどのために色々しているのですが、本質的な部分だけ簡潔に表示しています。

features/auth/components/login.tsx
			
			
import type { ComponentProps } from 'react'
import { Button } from '@/ui/button'
import { Input } from '@/ui/input'
import { Label } from '@/ui/label'

interface Props {
	handleSubmit: ComponentProps<'form'>['onSubmit']
}

export const Login = ({ handleSubmit }: Props) => {
	return (
		<form className="px-6 space-y-6" onSubmit={handleSubmit}>
			<div className="space-y-2">
				<Label htmlFor="username">email</Label>
				<Input
					name="username"
					type="email"
					required
					placeholder="email@example.com"
				/>
			</div>
			<div className="space-y-2">
				<Label htmlFor="password">password</Label>
				<Input name="password" type="password" required />
			</div>
			<div>
				<Button type="submit" variant="outline" className="mt-4 w-full">
					login
				</Button>
			</div>
		</form>
	)
}


		

Login コンポーネントを Page から呼び出します。
handleLoginでは下記の実装をします。

  1. FormData をログインで利用できるデータにパースする
  2. signIn を呼び出して、signInStep を取得する
  3. signInStep が DONE の場合はログインが完了したとみなす
  4. signInStep が DONE 以外の場合は追加の処理が必要になるので step という状態に値をセットする

省略していますが、step に応じてフォームの表示を切り替えて、パスワードや OTP を受け取ったりします。

app/login/page.tsx
			
			'use client'

import { confirmSignIn, signIn } from 'aws-amplify/auth'
import { useRouter } from 'next/navigation'
import { type FormEvent, useState } from 'react'
import z from 'zod'
import { ConfirmLogin } from '@/features/auth/components/confirm-login'
import { Login } from '@/features/auth/components/login'

const LoginSchema = z.object({
	username: z.email(),
	password: z.string(),
})

export default function LoginPage() {
	const [step, setStep] = useState('')
	const router = useRouter()

	const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
		event.preventDefault()
		const data = new FormData(event.currentTarget)
		const parsed = LoginSchema.safeParse({
			username: data.get('username')?.toString(),
			password: data.get('password')?.toString(),
		})
		if (!parsed.success) {
			// error handling
			console.error(parsed.error)
			return
		}
		const signInStep = await signIn(parsed.data)
			.catch((error) => {
				// error handling
				console.log(error)
				return null
			})
			.then((result) => result?.nextStep.signInStep)

		if (signInStep === 'DONE') {
			router.push('/')
			return
		}

		if (signInStep) {
			setStep(signInStep)
		}
	}

	if (step === 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED') {
      // step に応じた処理
	}
	return <Login handleSubmit={handleLogin} />
}


		

ユーザーグループに応じて表示を切り替える

今までの内容でユーザーログインをすることはできます。次はユーザー情報、ユーザーグループをユーザーの操作可能な権限グループとみなして、ページアクセス時に表示を切り替えられるようにします。今回は admin と viewer というユーザーグループを用意して、admin 権限の場合にのみ閲覧できるページを作ります。

まずは、下記のように CLI かマネジメントコンソールを経由してグループを追加します。

			
			aws cognito-idp admin-add-user-to-group \
    --user-pool-id <value> \
    --username <value> \
    --group-name <value>

		

次に、 /only-admin というページを追加します。
authorizations という配列に許可されるグループを記述して、それに該当しない場合は 401 を返すようにします。

app/(authenticated)/only-admin/page.tsx
			
			import { redirect, unauthorized } from 'next/navigation'
import { getUser } from '@/features/auth/utils/server'

const authorizations = ['admin']

const hasAuthorization = (
	authorizations: string[],
	groups: string[],
): boolean => {
	return groups.some((group) => authorizations.includes(group))
}

export default async function Home() {
	const user = await getUser().catch((error) => {
		console.error(error)
		redirect('/login')
	})

	if (!hasAuthorization(authorizations, user.groups)) {
		unauthorized()
	}

	return <pre>{JSON.stringify(user, null, 2)}</pre>
}


		

また、Next.js で 401 を利用するには experimental.authInterrupts を有効にする必要があるので next.config.ts で調整しましょう。これで localhost:3000/only-admin にアクセスした際にグループが含まれていたらページが表示されて、ない場合は 401 ページが表示されるようになりました。

これにてログインから、グループに応じたページ表示の切り替えまで完了しました。

付録. nextStep をハンドリングする

signIn メソッドなどのレスポンスに含まれる nextStep とそれに対する対処になります。
Cognito User Pool の設定と照らし合わせて必要になる処理を記述していきましょう。

ステップ名 説明 使用メソッド
DONE サインイン完了 N/A
CONFIRM_SIGN_UP アカウント未確認 confirmSignUp({ username, confirmationCode })
RESET_PASSWORD パスワードリセット必要 resetPassword({ username }) / confirmResetPassword({username, confirmationCode, newPassword})
CONFIRM_SIGN_IN_WITH_PASSWORD パスワード入力必要 confirmSignIn({ challengeResponse})
CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED 新パスワード設定必要 confirmSignIn({ challengeResponse})
CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE カスタムチャレンジ認証 confirmSignIn({ challengeResponse})
CONFIRM_SIGN_IN_WITH_SMS_CODE SMS認証コード必要 confirmSignIn({ challengeResponse})
CONFIRM_SIGN_IN_WITH_TOTP_CODE TOTPコード必要 confirmSignIn({ challengeResponse})
CONFIRM_SIGN_IN_WITH_EMAIL_CODE メール認証コード必要 confirmSignIn({ challengeResponse})
CONTINUE_SIGN_IN_WITH_MFA_SELECTION MFA方式の選択 confirmSignIn({ challengeResponse})
CONTINUE_SIGN_IN_WITH_TOTP_SETUP TOTP設定が必要 confirmSignIn({ challengeResponse})
CONTINUE_SIGN_IN_WITH_EMAIL_SETUP メールMFA設定が必要 confirmSignIn({ challengeResponse})
CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION MFA設定方法の選択 confirmSignIn({ challengeResponse})
CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION 第一認証要素の選択 confirmSignIn({ challengeResponse})

付録. AuthError を処理する

aws-amplify/authsignIn などの処理でエラーが起きたら AuthError インスタンスのエラーが返ってきます。
これ自体の処理は下記のように状態として保持して表示すれば良いです。下記のようにハンドラーでエラーをハンドリングして、表示させます。

			
			
export default function LoginPage() {
	const [step, setStep] = useState('')
	const [error, setError] = useState('')

	const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
		event.preventDefault()
		const data = new FormData(event.currentTarget)

		const parsed = LoginSchema.parse({
			username: data.get('username')?.toString(),
			password: data.get('password')?.toString(),
		})
		const signInStep = await signIn(parsed)
			.catch((error) => {
			    // AuthError の場合に setError を実行する
				if (error instanceof AuthError) {
					setError(error.message)
				}
				logger.info(error)
				return null
			})
			.then((result) => result?.nextStep.signInStep)
    // ...
    }

	return (
		<div className="flex flex-col gap-4 min-h-svh w-full items-center justify-center max-w-sm mx-auto">
			{error !== '' && (
				<Alert variant="destructive">
					<AlertCircleIcon />
					<AlertTitle>ログインに失敗しました</AlertTitle>
					<AlertDescription>{error}</AlertDescription>
				</Alert>
			)}
			{(() => {
				switch (step) {
					case 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED':
						return <ConfirmLogin handleSubmit={handleConfirmSignIn} />
					default:
						return <Login handleSubmit={handleLogin} />
				}
			})()}
		</div>
	)


		

実際にエラーを発生させた場合には下記のように表示がされます。

メッセージを単純に表示できれば良い場合は AuthError.message をそのまま表示すれば良いですが、英語での表記になるので、日本語で表示したいなどの要件が出てきます。その場合は AuthError.name が下記のように返ってきます。これは Cognito が投げる例外なのでそれをキーに翻訳を掛ければ良いです。

エラー名 発生場面
UserNotFoundException サインイン時、ユーザー名が間違っている場合
NotAuthorizedException パスワードが間違っている、トークンが無効な場合
UserNotConfirmedException メール確認が完了していない場合
PasswordResetRequiredException 管理者がパスワードリセットを要求した場合
UsernameExistsException サインアップ時、既存のユーザー名を使用した場合
InvalidPasswordException パスワードポリシーを満たしていない場合
InvalidParameterException 必須項目が不足、形式が不正な場合
CodeMismatchException 間違った確認コードを入力した場合
ExpiredCodeException 確認コードの有効期限が切れた場合
CodeDeliveryFailureException SMS/メール配信に失敗した場合
TooManyRequestsException API呼び出し制限を超えた場合
TooManyFailedAttemptsException ログイン試行回数を超えた場合
LimitExceededException リソース制限に達した場合
InvalidUserPoolConfigurationException Cognito設定に問題がある場合
NetworkError インターネット接続に問題がある場合
ResourceNotFoundException 指定したリソースが存在しない場合
AliasExistsException メール/電話番号が既に使用されている場合
InternalErrorException AWS側のシステムエラー
InvalidLambdaResponseException カスタムLambdaトリガーのエラー

さいごに

Cognito User Pools と Amplify を利用したログインの実装について、最初はとてもとっつきにくい印象を受けました。ライブラリがどのような役割をしているかを理解することで一気に理解しやすくなりました。ぜひみなさんも使う際は参考にしてください。

この記事をシェアする

FacebookHatena blogX

関連記事

Amplify Adapter Next.js と Cognito でログインをしてみた | DevelopersIO