0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自前UIのGoogleログインが「サインイン処理中」で止まる — Amplify v6 enable-oauth-listener と trailingSlash の罠

0
Posted at

はじめに

Next.js(output: 'export' の SPA=単一ページで動くアプリ)に「Google でサインイン」を付けた。認証基盤は Amazon Cognito、その Hosted UI 経由で Google にログインさせる。ログインボタンだけは自前 UI にして、Amplify の <Authenticator> は使わない。SDK は aws-amplify v6(6.17.0)。

ところが、Google 認証自体は成功して /callback まで戻ってくる。なのに画面は「サインイン処理中…」のまま永久に固まるsuccess にも error にもならない。原因は2段階あった。本稿はその原因と直し方を、OAuth の用語から噛み砕いて残す。

この記事を貫くテーマは、次のとおり。

  • 「code を受け取る」と「code をトークンに交換する」は別物fetchAuthSession は取り出すだけ、交換はリスナーの仕事。
  • Amplify v6 の自前 UI ではリスナーを自分で有効化するenable-oauth-listener の import が必須。加えて trailingSlash: true の 308 が交換のきっかけを奪う。

1. 前提知識:そもそも何が起きているのか(OAuth の流れ)

「Google でログイン」の裏側は OAuth 2.0 Authorization Code Flow + PKCE(PKCE=横取りされた code を他人に使われないようにする拡張)。難しそうだが、“合鍵を直接渡さず、引換券を経由して受け取る” と考えると分かりやすい。

登場人物 役割
自分のアプリ(SPA) ログインボタンと、戻り先の /callback ページを持つ
Cognito Hosted UI AWS の認証窓口。Google との仲介もしてくれる
Google 実際にユーザー本人確認をする所
Amplify(SDK) 上記とのやり取りをフロント側で代行するライブラリ

肝は ★のトークン交換/callback?code=...codeただの引換券で、これ単体ではログイン完了にならない。裏でトークンに交換する処理が走って初めてログイン成功になる。今回の不具合は、この★が走らないことが2段階の原因で起きていた。


2. 実装の全体像(実際のコード)

サインイン開始(sign-in/page.tsx

'use client';
import { signInWithRedirect } from 'aws-amplify/auth';
import { configureAmplify } from '@/lib/amplify';

const SignInPage = () => {
  useEffect(() => { configureAmplify(); }, []);
  const handleGoogleSignIn = () => {
    void signInWithRedirect({ provider: 'Google' }); // → Hosted UI へ飛ぶ
  };
  // ...ボタン
};

Amplify の設定(lib/amplify.ts / auth-config.ts

env から Cognito の値を読んで Amplify.configure する純関数。responseType: 'code'(= Authorization Code Flow)、scope は openid email profile

コールバック(callback/page.tsx

戻ってきた /callback?code=... で、セッションが取れたら成功表示。

const session = await fetchAuthSession();
const idToken = session.tokens?.idToken;
if (idToken !== undefined) { setStatus('success'); }

ここまでは一見正しい。だが fetchAuthSession の役割を誤解していると、この後で固まる。


3. ハマり①:trailingSlash: true の 308 リダイレクトでトークン交換が消える

最初の症状は「/callback に戻った瞬間に固まる」。next.config.mjstrailingSlash: true が入っていた。

そもそも trailingSlash とは

trailingSlashNext.js の設定で、URL の末尾にスラッシュ(/)を付けるかどうかを決める。trailingSlash: true にすると、Next.js は末尾スラッシュなしの URL を、ありの URL へ強制リダイレクトする。

  • trailingSlash: false(デフォルト):/callback はそのまま /callback
  • trailingSlash: true/callback にアクセスすると /callback/308 リダイレクトされる

なぜわざわざ付けるのか。理由は静的ホスティングとの相性だ。output: 'export'(静的書き出し)で S3 / CloudFront に置くとき、/callback/index.html というディレクトリ構造の URL に揃えたい。そのために true にしていることが多い。つまりそれ自体は珍しくない、ごく普通の設定だ。だからこそ気づきにくい。

ちなみに 308 は「Permanent Redirect(恒久的リダイレクト)」を表す HTTP ステータス。301 と似ているが、308 はリダイレクト後も元の HTTP メソッドとボディを維持する点が違う。ブラウザは黙って新しい URL へ飛び直す。

何が問題だったか

Google から戻ってくる URL は /callback?code=...&state=...。ところが trailingSlash: true だと、これがページとして読まれる前に /callback/?code=... へ 308 で飛ばされる

問題はこの「飛ばされる」タイミングにある。Amplify はページ読み込み時に、URL の code を見てトークン交換を始める。だが 308 で別 URL に飛び直す一拍のあいだに、そのきっかけを取り逃がす。結果、交換が走らないまま固まる。

直し方

trailingSlash: true を外す(意図をコメントで残す)。

// OAuth コールバックを /callback で一致させるため末尾スラッシュは付けない(PB-10)。
// trailingSlash:true だと /callback → /callback/ に 308 され、Amplify のコード交換が走らず
// 「サインイン処理中…」で止まる。
// trailingSlash: true,  ← 削除

※ Cognito 側のコールバック URL 許可リストと末尾スラッシュの有無まで一致させること。/callback/callback/ は別物として扱われる。

これで一歩進んだ……が、まだ固まった。ここからが本番。


4. ハマり②(本命):enable-oauth-listener の import が抜けていた

症状の整理

まず、何が起きていて何が起きていないのかを切り分ける。

  • URL は正しく /callback?code=...&state=... まで戻っている(= Google も Cognito も成功)。
  • なのに success にも error にもならず、ずっと pending

つまり ★のトークン交換が一切走っていない

なぜ走らないのか — Amplify v6 の内部挙動

ここが今回いちばん理解が要った所。fetchAuthSession()「もう保存済みのトークンを取り出す」だけの関数で、URL の ?code= を見て交換する処理ではない

では誰が交換するのか。v6 では、URL に code があるのを検知して交換する処理は専用の OAuth リスナーが担当する。そして自前 UI(<Authenticator> を使わない)の場合、そのリスナーは勝手には登録されない。明示的に副作用 import して有効化する:

import 'aws-amplify/auth/enable-oauth-listener';

そもそも enable-oauth-listener とは

aws-amplify/auth/enable-oauth-listener は、aws-amplify v6 が用意している**「OAuth リスナーを登録するためだけのサブモジュール」。読み込むと OAuth リスナーが有効化される**。このリスナーが signInWithRedirect の戻り(?code=... 付きでページに戻ってきた状態)を監視し、自動でトークン交換を始める。

なぜ v6 でわざわざ別 import になったのか。v5 までは Auth を設定した時点でこのリスナーが自動で登録されていた。v6 ではライブラリがモジュール分割された。使わない機能を最終バンドルから削れるようにする、いわゆるツリーシェイキングのためだ。その結果、OAuth リスナーは「使う人だけ明示的に有効化する」方式に変わった<Authenticator> コンポーネントはこの import を内部で済ませてくれる。だから自前 UI のときだけ、自分で書く必要がある。

「副作用 import」とはimport { foo } from 'x' のように値を受け取らない import のこと(import 'x' の形)。これは「x から何かを使うため」ではなく、「x が読み込まれた瞬間に走るトップレベルの処理(=副作用)を起こすため」に書く。ここでは読み込まれた瞬間にリスナー登録が実行されるのが、その副作用にあたる。

この import が無いと、次の連鎖で pending に落ちる。

たとえ話:引換券(code)を握って受付(callback ページ)まで来たのに、「引換券→商品の交換窓口の担当者(OAuth リスナー)」をそもそも出勤させていなかったimport 'aws-amplify/auth/enable-oauth-listener' は、その担当者を出勤させる一行。

直し方

コールバックページの先頭で副作用 import するだけ。

'use client';

// 自前 UI で signInWithRedirect の戻り(?code=)を完了させるには
// この副作用 import が必須。無いとトークン交換リスナーが登録されず「サインイン処理中…」で固まる。
import 'aws-amplify/auth/enable-oauth-listener';
import { fetchAuthSession } from 'aws-amplify/auth';
// ...

テストの注意:副作用 import(読み込んだ瞬間にリスナー登録が走る)なので、単体テストでは実体を読み込ませないようモックする。
vi.mock('aws-amplify/auth/enable-oauth-listener', () => ({}))


5. 「なぜこれで成功したのか」総まとめ

直したのは結局2行(+設定1箇所)。効いた理由を一言で:

  1. trailingSlash を外した → 戻りの URL が 308 で飛ばされず、/callback?code=... のままページが読まれる(交換の“きっかけ”を取り逃がさない)。
  2. enable-oauth-listener を import した → URL の code を検知して交換する担当者を出勤させた。交換が走り、Hub(Amplify がログイン等の出来事を通知する仕組み)の signedIn が発火し、fetchAuthSession() が idToken を返し、「サインインしました」に到達した。

要するに 「code を受け取ること」と「code をトークンに交換すること」は別物だ。v6 の自前 UI では、後者を自分で有効化する必要があった。これが核心である。


6. 検証時にハマりやすい点

直し終えても、再現テストの段階で別の罠を踏みやすい。先に潰しておく。

  • URL の code はワンタイム(一度使うと無効/短時間で失効)。失敗した /callback?code=...リロードしても直らない。必ず /sign-in からやり直す。
  • Cognito のコールバック URL 許可リストと、アプリの実 URL(末尾スラッシュ含め)を一致させる。
  • 複数 origin(localhostdev.app 等)を許可する場合は env をカンマ区切りで配列化する。

おわりに

「Google でログイン」が固まったとき、つい fetchAuthSession の呼び方を疑ってしまう。だが本当の境目は 「code を受け取る」と「code を交換する」が別の仕事だという点にある。Amplify v6 の自前 UI では、交換を担うリスナーを自分で出勤させる必要がある。

  • 「code を受け取る」と「code をトークンに交換する」は別物
  • v6 の自前 UI ではリスナーを自分で有効化する(+ trailingSlash 308 の罠)

同じ「サインイン処理中…」で固まっている人は、まず enable-oauth-listener の import と trailingSlash を確認してほしい。次の一歩としては、Hub の認証イベント購読や、サインアウト・トークン更新まわりの実装に進むとよい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?