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?

Cognito IDトークンの `cognito:groups` が取得できない — Base64urlデコードの落とし穴

0
Last updated at Posted at 2026-09-08

Cognito IDトークンの cognito:groups が取得できない — Base64urlデコードの落とし穴

要約

Amazon Cognitoで admin グループに所属しているのに、フロントエンドで cognito:groups が空になる事象が発生。原因はブラウザの atob() がBase64url形式のJWTペイロードを正しくデコードできないケースがあること。ユーザーごとにトークンの長さが変わるため、特定ユーザーだけ再現するのが厄介なポイント。

前提:構成と背景

社内向けのWebアプリケーション(SPA)を以下の構成で構築していました。

[ブラウザ] → [ALB] → [Lambda] → [DynamoDB]
                                → [Cognito User Pool]
  • 認証は Amazon Cognito User Pool で管理
  • フロントエンドは vanilla JS のSPA(S3に配置し、ALB経由で配信)
  • ログイン時に Cognito の InitiateAuthUSER_PASSWORD_AUTH)を呼び、返却されたIDトークンから cognito:groups を読み取ってUIを出し分ける
  • admin グループに所属するユーザーのみ管理画面へのリンクを表示する仕様

事象

管理者用のCognitoグループ admin にユーザーを追加したにも関わらず、特定のユーザーだけ管理画面へのリンクが表示されない。

ユーザー adminグループ所属 管理リンク表示
admin@example.com 所属 表示される
another_admin@example.com 所属 表示されない
  • Cognito CLI で admin-list-groups-for-user → 両ユーザーとも admin グループに所属していることを確認済み
  • ログアウト → 再ログイン、localStorage クリア → 再ログインを試しても解消しない
  • ユーザーを削除して小文字で再作成しても解消しない

切り分け

Cognito側の確認

CLIから直接認証を実行し、発行されるIDトークンのペイロードを確認した。

TOKEN=$(aws cognito-idp initiate-auth \
  --client-id ${CLIENT_ID} \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters USERNAME="another_admin@example.com",PASSWORD="パスワード" \
  --region ap-northeast-1 \
  --query "AuthenticationResult.IdToken" \
  --output text)

echo $TOKEN | python3 -c "
import sys, json, base64
token = sys.stdin.read().strip()
payload = token.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
data = json.loads(base64.urlsafe_b64decode(payload))
print(json.dumps(data, indent=2, ensure_ascii=False))
"

結果:両ユーザーとも cognito:groups: ["admin"] がIDトークンに含まれている

→ Cognito側は正常。問題はフロントエンドのトークン処理にある。

フロントエンド側の確認

ブラウザの localStorage に保存された currentUser オブジェクトを確認すると:

// admin@example.com でログイン後
{ email: "admin@example.com", groups: ["admin"], ... }

// another_admin@example.com でログイン後
{ email: "another_admin@example.com", groups: [], ... }  // ← 空!

IDトークンには cognito:groups が含まれているのに、ブラウザでデコードした結果は空。ここでフロントエンドのデコード処理にバグがあると確定。

原因

問題のコード

// IDトークンからグループ情報を取得
let groups = [];
if (idToken) {
  try {
    groups = JSON.parse(atob(idToken.split(".")[1]))["cognito:groups"] || [];
  } catch {}
}

なぜ失敗するのか

CognitoのIDトークン(JWT)のペイロード部分は Base64url エンコードされている。一方、ブラウザの atob()標準Base64 を期待する。

Base64url 標準Base64
+ の代替 - +
/ の代替 _ /
パディング = なし あり

JWTペイロードの長さが4の倍数でないとき、atob() はパディング不足で例外を投げる。しかし catch {} で例外を握り潰しているため、静かに groups = [] のまま処理が進む

なぜ特定ユーザーだけ再現するのか

IDトークンのペイロードにはユーザー固有の情報(sub(UUID)、emailnicknamejti 等)が含まれる。これらの値の長さはユーザーごとに異なるため、Base64エンコード後の文字列長も変わる。

  • パディングなしでも4の倍数になるユーザー → atob() が正常動作 → 問題に気づかない
  • 4の倍数にならないユーザー → atob() が例外 → groups が空のまま

つまり 「たまたま動くユーザー」と「動かないユーザー」が存在する という再現性の低い厄介なバグになる。

対応

Base64url → 標準Base64 の変換とパディング追加を行ってからデコードする。加えて、マルチバイト文字(日本語ニックネーム等)に対応するためUTF-8デコードも追加。

修正後のコード

// IDトークンからグループ情報を取得
let groups = [];
if (idToken) {
  try {
    const b64 = idToken.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
    const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
    const json = decodeURIComponent(
      atob(b64 + pad)
        .split("")
        .map(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
        .join("")
    );
    groups = JSON.parse(json)["cognito:groups"] || [];
  } catch {}
}

修正のポイント

  1. -+_/ : Base64url を標準Base64に変換
  2. パディング追加 : 文字列長が4の倍数になるよう = を補完
  3. UTF-8デコード : atob() の出力をバイト列として扱い、decodeURIComponent + パーセントエンコードでUTF-8文字列に復元

切り分けの過程(時系列)

この問題は原因特定までに複数のステップを要した。同じ状況に遭遇した方の参考になるよう、切り分けの流れを記録しておく。

  1. グループ所属の確認 → CLI で確認。問題なし
  2. ログアウト → 再ログイン → 解消せず
  3. localStorage クリア → 再ログイン → 解消せず
  4. User Pool Client 設定確認(ReadAttributes, ExplicitAuthFlows)→ 問題なし
  5. メールアドレスの大文字/小文字の影響を疑い、小文字で再作成 → 解消せず
  6. CLIから直接 initiate-auth を実行し、IDトークンのペイロードを確認cognito:groups は含まれていた
  7. Cognito側は正常、フロントエンドのデコード処理が原因と特定
  8. atob() のBase64urlパディング問題を修正 → 解消

ポイントは手順6の「CLIで同じ認証フローを再現してIDトークンの中身を直接確認する」こと。これでCognito側かフロントエンド側かを切り分けられた。

教訓

  • catch {} で例外を黙殺するのは危険。少なくとも console.warn を入れておけばすぐ気づけた
  • JWTのデコードには atob() を直接使わない。Base64url対応のヘルパー関数を用意すべき
  • ブラウザ環境なら jwt-decode のような軽量ライブラリを使うのも選択肢
  • 「あるユーザーでは動くが別のユーザーでは動かない」場合、ユーザー固有のデータ長に依存する処理を疑う

参考

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?