はじめに
Next.js と AWS Cognito を使った Web アプリケーションに、Google ソーシャルログイン機能を実装しました。
この記事では、実装手順と実際に遭遇した問題、その解決方法をまとめます。
なぜソーシャルログインを実装したか
ユーザーが簡単にログインできるようにするため、Google アカウントでのログイン機能を追加しました。
メールアドレスとパスワードを覚える必要がなくなり、ユーザー体験が向上します。
実装の概要
使用技術
- Next.js 14 (App Router)
- AWS Cognito User Pool (認証基盤)
- Google OAuth 2.0 (ソーシャルログインプロバイダー)
- TypeScript
アーキテクチャ
ユーザー
→ ログインページ
→ Cognito Hosted UI
→ Google認証
→ コールバック
→ トークン取得
→ ホーム画面
- ユーザーが「Google でログイン」ボタンをクリック
- Cognito の認証 URL にリダイレクト
- Google の認証画面でログイン
- コールバック URL にリダイレクト
- 認証コードをトークンに交換
- ユーザー情報を取得してログイン完了
実装手順
① Google Cloud Console での設定
OAuth 同意画面の設定
- Google Cloud Console にアクセス
- プロジェクトを作成または選択
- 「API とサービス」→「OAuth 同意画面」を選択
- ユーザータイプを「外部」に設定(一般ユーザーが利用可能)
- アプリ情報を入力:
- アプリ名
- ユーザーサポートメール
- デベロッパーの連絡先情報
- スコープを設定:
openidemailprofile
重要: スコープは後で追加できますが、最初に設定しておくとスムーズです。
OAuth 2.0 クライアント ID の作成
- 「API とサービス」→「認証情報」を選択
- 「認証情報を作成」→「OAuth クライアント ID」を選択
- アプリケーションの種類: **「ウェブアプリケーション」**を選択
- 承認済みのリダイレクト URIを追加:
- 開発環境:
http://localhost:3000/api/auth/callback - 本番環境:
https://yourdomain.com/api/auth/callback - 重要: AWS Cognito の Hosted UI を使用する場合、Cognito のコールバック URL も追加:
https://your-cognito-domain.auth.ap-northeast-1.amazoncognito.com/oauth2/idpresponse
- 開発環境:
- 「作成」をクリック
- クライアント ID とシークレットをコピー(シークレットはこの時点でしか表示されません)
② AWS Cognito の設定
Identity Provider の設定
- AWS Cognito コンソールで User Pool を開く
- 「サインインエクスペリエンス」→「フェデレーション」→「Identity providers」を選択
- 「Google」を選択
- Google Cloud Console で取得したクライアント ID とシークレットを入力
- 「属性マッピング」を設定:
- Google の
email→ Cognito のemail - Google の
name→ Cognito のname - Google の
sub→ Cognito のusername
- Google の
- 「保存」をクリック
App Client の設定
- 「アプリの統合」→「アプリクライアント」を選択
- 既存の App Client を編集
- 「ホストされた UI」を有効化
- 「コールバック URL」を設定:
- 開発環境:
http://localhost:3000/api/auth/callback - 本番環境:
https://yourdomain.com/api/auth/callback
- 開発環境:
- 「許可された OAuth スコープ」で以下を選択:
openidemailprofile
Cognito Domain の確認
- 「ブランディング」→「ドメイン」を選択
- Cognito ドメインを確認または作成
- ドメイン名をコピー(例:
your-app.auth.ap-northeast-1.amazoncognito.com)
注意: プロトコル(https://)は含めず、ドメイン部分のみを使用します。
③ 環境変数の設定
.env.local に以下を追加:
# 既存の環境変数
COGNITO_CLIENT_ID=your-client-id
AWS_REGION=ap-northeast-1
# ソーシャルログイン用の環境変数
COGNITO_DOMAIN=your-app.auth.ap-northeast-1.amazoncognito.com # プロトコル不要
COGNITO_REDIRECT_URI=http://localhost:3000/api/auth/callback
重要: 以下の 3 つの場所でリダイレクト URI が一致している必要があります:
.env.localのCOGNITO_REDIRECT_URI- AWS Cognito の App Client 設定
- Google Cloud Console の OAuth 2.0 クライアント ID 設定
④ バックエンド実装
認証 URL 生成 API
app/api/auth/social/route.ts を作成:
import { NextRequest, NextResponse } from "next/server";
import { getAuthorizationUrl } from "@/lib/cognito";
import { randomBytes } from "crypto";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const provider = searchParams.get("provider");
if (!provider) {
return NextResponse.json(
{ error: "プロバイダーが指定されていません" },
{ status: 400 }
);
}
// CSRF 対策用の state を生成
const state = randomBytes(32).toString("base64url");
// 認証 URL を生成
const authUrl = getAuthorizationUrl(provider, state);
return NextResponse.json({
authUrl,
state,
});
}
コールバック処理 API
app/api/auth/callback/route.ts を作成:
import { NextRequest, NextResponse } from "next/server";
import { exchangeCodeForTokens, getUserInfoFromIdToken } from "@/lib/cognito";
import { dynamoClient, TABLES } from "@/lib/dynamodb";
import { GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
import { getCurrentTimestamp } from "@/lib/utils";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get("code");
const errorParam = searchParams.get("error");
if (errorParam) {
return NextResponse.redirect(
new URL(`/callback?error=${encodeURIComponent(errorParam)}`, request.url)
);
}
if (!code) {
return NextResponse.redirect(
new URL("/callback?error=認証コードが取得できませんでした", request.url)
);
}
// 認証コードをトークンに交換
const tokens = await exchangeCodeForTokens(code);
// ID Tokenからユーザー情報を取得
const userInfo = getUserInfoFromIdToken(tokens.idToken);
const userId = userInfo.userId || "";
// DynamoDBにユーザーが存在しない場合は作成
if (userId) {
const existingUser = await dynamoClient.send(
new GetCommand({
TableName: TABLES.USERS,
Key: { userId },
})
);
if (!existingUser.Item) {
const timestamp = getCurrentTimestamp();
await dynamoClient.send(
new PutCommand({
TableName: TABLES.USERS,
Item: {
userId,
username: userInfo.username || userInfo.email || "",
email: userInfo.email || "",
displayName: userInfo.displayName || userInfo.username || "",
createdAt: timestamp,
updatedAt: timestamp,
},
})
);
}
}
// トークンをクエリパラメータとして /callback にリダイレクト
const redirectUrl = new URL("/callback", request.url);
redirectUrl.searchParams.set("accessToken", tokens.accessToken);
redirectUrl.searchParams.set("idToken", tokens.idToken);
redirectUrl.searchParams.set("userId", userId);
redirectUrl.searchParams.set("username", userInfo.username || "");
return NextResponse.redirect(redirectUrl);
}
Cognito ライブラリの拡張
lib/cognito.ts に以下を追加:
/**
* ソーシャルログイン用の認証 URL を生成
*/
export function getAuthorizationUrl(provider: string, state: string): string {
let domain = process.env.COGNITO_DOMAIN;
const clientId = getClientId();
const redirectUri = process.env.COGNITO_REDIRECT_URI;
// プロトコルが含まれている場合は削除
domain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
// プロバイダー名を正規化(Googleの場合は大文字に)
const normalizedProvider =
provider.toLowerCase() === "google" ? "Google" : provider;
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
identity_provider: normalizedProvider,
state: state,
});
return `https://${domain}/oauth2/authorize?${params.toString()}`;
}
/**
* 認証コードを Cognito トークンに交換
*/
export async function exchangeCodeForTokens(code: string) {
let domain = process.env.COGNITO_DOMAIN;
const clientId = getClientId();
const redirectUri = process.env.COGNITO_REDIRECT_URI;
domain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
const tokenUrl = `https://${domain}/oauth2/token`;
const params = new URLSearchParams({
grant_type: "authorization_code",
client_id: clientId,
code: code,
redirect_uri: redirectUri,
});
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params.toString(),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`トークン交換に失敗しました: ${errorText}`);
}
const data = await response.json();
return {
accessToken: data.access_token,
idToken: data.id_token,
refreshToken: data.refresh_token,
};
}
/**
* ID Tokenからユーザー情報を取得(ソーシャルログイン用)
*/
export function getUserInfoFromIdToken(idToken: string) {
try {
const payload = JSON.parse(
Buffer.from(idToken.split(".")[1], "base64").toString()
);
return {
userId: payload.sub || null,
username:
payload.preferred_username || payload.email || payload.sub || "",
email: payload.email || "",
displayName: payload.name || payload.preferred_username || "",
};
} catch (error) {
console.error("Failed to decode ID token:", error);
return {
userId: null,
username: "",
email: "",
displayName: "",
};
}
}
⑤ フロントエンド実装
ログインページの拡張
app/(auth)/login/page.tsx に Google ログインボタンを追加:
const handleGoogleLogin = async () => {
setSocialLoading(true);
setError("");
try {
// 認証 URL を取得
const response = await fetch("/api/auth/social?provider=google");
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "認証 URL の取得に失敗しました");
}
// state をセッションストレージに保存(CSRF 対策)
if (data.state) {
sessionStorage.setItem("oauth_state", data.state);
}
// 認証 URL にリダイレクト
if (data.authUrl) {
window.location.href = data.authUrl;
}
} catch (err: any) {
setError(err.message);
setSocialLoading(false);
}
};
コールバック処理ページ
"use client";
import { useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useAuthStore } from "@/lib/store/auth-store";
export default function CallbackPage() {
const searchParams = useSearchParams();
const setAuth = useAuthStore((state) => state.setAuth);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const handleCallback = async () => {
const accessToken = searchParams.get("accessToken");
const idToken = searchParams.get("idToken");
const userId = searchParams.get("userId");
const username = searchParams.get("username");
const errorParam = searchParams.get("error");
if (errorParam) {
setError(`認証エラー: ${errorParam}`);
return;
}
if (accessToken && idToken) {
// トークンをストアに保存
setAuth({
accessToken,
idToken,
userId: userId || "",
username: username || "",
});
// ホームページにリダイレクト
window.location.href = "/home";
}
};
handleCallback();
}, [searchParams, setAuth]);
// ... エラー表示とローディング表示
}
実際に遭遇した問題と解決方法
invalid_scope エラー
問題: Google ログイン時に「Some requested scopes were invalid」エラーが発生。
原因: Google Cloud Console の OAuth 同意画面で、必要なスコープ(openid, email, profile)が有効になっていなかった。
解決方法:
- Google Cloud Console の「OAuth 同意画面」を開く
- 「スコープを追加または削除」をクリック
- 以下のスコープを追加:
openidemailprofile
- テストユーザーを追加(開発中の場合)
405 エラー(Method Not Allowed)
問題: Cognito からのリダイレクト時に 405 エラーが発生。
原因: /api/auth/callback が POST メソッドのみをサポートしていたが、Cognito からのリダイレクトは GET リクエスト。
解決方法: /api/auth/callback/route.ts に GET メソッドを追加し、クエリパラメータから認証コードを取得するように修正。
Access Token does not have required scopes
問題: getUser() 関数で「Access Token does not have required scopes」エラーが発生。
原因: ソーシャルログインで取得した Access Token には aws.cognito.signin.user.admin スコープが含まれていないため、GetUserCommand が使用できない。
解決方法: ID Token(JWT)から直接ユーザー情報を取得する getUserInfoFromIdToken() 関数を実装し、ソーシャルログイン時はこちらを使用。
初回ログイン時の 404 エラー
問題: 初回 Google ログイン時に /api/users/[userId] で 404 エラーが発生。
原因: Cognito ユーザープールにはユーザーが作成されるが、DynamoDB のユーザーテーブルにはまだレコードが存在しない。
解決方法: コールバック処理で、DynamoDB にユーザーが存在しない場合は自動的に作成する処理を追加。
Cognito Hosted UI の画面が表示される
問題: Google ログイン時に、Cognito の Hosted UI 画面が表示されてから Google 認証に進む。
原因: identity_provider パラメータの値が小文字の "google" だったが、Cognito の設定では大文字の "Google" が期待されていた。
解決方法: プロバイダー名を正規化し、Google の場合は大文字の "Google" に変換。
実装のポイント
CSRF 対策
認証 URL 生成時にランダムな state パラメータを生成し、セッションストレージに保存。コールバック時に state を検証することで、CSRF 攻撃を防ぎます。
ID Token からのユーザー情報取得
ソーシャルログインでは、Access Token に必要なスコープがないため、ID Token(JWT)から直接ユーザー情報を取得します。
ID Token には以下の情報が含まれています:
sub: ユーザー IDemail: メールアドレスname: 表示名preferred_username: ユーザー名
初回ログイン時のユーザー作成
ソーシャルログインで初めてログインしたユーザーは、Cognito ユーザープールには自動的に作成されますが、アプリケーション側の DynamoDB にはまだレコードが存在しません。
コールバック処理で、ユーザーが存在しない場合は自動的に作成することで、シームレスな体験を提供します。
まとめ
AWS Cognito と Google OAuth 2.0 を使用したソーシャルログインの実装は、設定が複雑に見えますが、一度設定すれば安定して動作します。特に以下の点に注意が必要です:
- リダイレクト URI の一致: 3 つの場所(環境変数、Cognito、Google)で一致させる
- スコープの設定: Google Cloud Console で必要なスコープを有効化
- ID Token の活用: ソーシャルログインでは ID Token からユーザー情報を取得
- 初回ログイン時の処理: DynamoDB へのユーザー作成を自動化
実装後は、ユーザーが簡単にログインできるようになり、ユーザー体験が大幅に向上しました。
参考リソース
- Google Cloud Console
- Google OAuth 2.0 設定ガイド
- AWS Cognito Hosted UI ドキュメント
- AWS SDK for JavaScript v3 – Cognito Identity Provider
- OAuth 2.0 仕様


