Cognito で3キャリア + Google のアカウント統合を設計する

ドコモ(OIDC)・au(OpenID 2.0)・ソフトバンク(Yahoo! JAPAN OIDC)・Google・メール+パスワードの5つのログイン方法を、Cognitoの1ユーザーに統合する設計。各キャリアのIdP仕様の違い、SECRET_HASHの計算、opaque IDの扱い、PreSignUp Lambdaによる自動リンクまで。

1人のユーザーが、メール+パスワード、Google、ドコモ、au、ソフトバンクのどれでログインしても同じアカウントにたどり着く — これをCognito User Poolの上で実現する設計を整理する。


目標

メール+パスワード ──┐
Google OIDC ───────┤
ドコモ OIDC ───────┤──→ 1つの cognito:username ──→ 1つのアカウント
Yahoo!/SB OIDC ────┤
au OpenID 2.0 ─────┘

3キャリアのIdP仕様

まず現状を整理する。キャリアによって対応プロトコルと取得できる情報が全く違う。

ドコモauソフトバンク
プロトコルOIDC (dアカウント・コネクト)OpenID 2.0Yahoo! JAPAN YConnect v2 (OIDC)
電話番号取得可(有料属性)取得不可要特別申請
メール取得可(要認証取得)取得不可取得可(openid email scope)
返ってくるIDOIDC sub(安定)opaque ID(不安定)OIDC sub(安定)
署名方式HS256N/ARS256
開発者登録申請制(審査3営業日)契約必要セルフサービス
ユーザー数dアカウント 9000万+Yahoo! JAPAN ID 5000万+

ドコモ: dアカウント・コネクト

正規のOIDC。Cognitoに外部IdPとして直接登録できる。電話番号が取れるので、既存アカウントとの自動マッチングが可能。

ただしIDトークンの署名がHS256(共有シークレット方式)。CognitoのOIDC IdP連携がHS256を受け付けるか要検証。ダメな場合はrellf-auth側でトークンを検証してRS256で再署名するラッパが必要。

エンドポイント:
  Authorization: https://id.smt.docomo.ne.jp/cgi8/oidc/authorize
  Token:         https://conf.uw.docomo.ne.jp/common/token
  UserInfo:      https://conf.uw.docomo.ne.jp/common/userinfo
  Issuer:        https://conf.uw.docomo.ne.jp/

特筆すべきは回線認証(かいせんにんしょう)。ドコモ回線からアクセスすると、SIM情報で自動認証されてパスワード入力不要。Wi-Fi経由の場合はID+パスワード。

au: OpenID 2.0(まだ)

一番厄介。2026年時点でもOpenID 2.0のまま。返ってくるのは:

電話番号もメールも返さない。 opaque IDはau IDに紐づくが、au ID再設定やMNPで変わる可能性がある。

Cognitoに繋ぐにはOIDCラッパが必要:

au OpenID 2.0 → rellf-auth (ラッパ) → 自前OIDC IDトークン発行 → Cognito外部IdP

ソフトバンク: Yahoo! JAPAN YConnect v2

SoftBankユーザーは「スマートログイン」でYahoo! JAPAN IDと電話番号が紐づいている。サードパーティ向けにはYahoo! JAPAN YConnect v2(正規OIDC、RS256署名)を使う。

エンドポイント:
  Discovery: https://auth.login.yahoo.co.jp/yconnect/v2/.well-known/openid-configuration
  Issuer:    https://auth.login.yahoo.co.jp/yconnect/v2/

メールは取れる。電話番号は標準スコープでは取れず、特別申請が必要。


Cognito 上のデータ構造

5つのログイン方法を統合すると、Cognitoのユーザーデータはこうなる。

AdminGetUser の結果:

Username: "13b99ebb-851c-4401-ba4e-a6aba782f139"   ← cognito:username(内部ID)
UserStatus: CONFIRMED
Enabled: true

Attributes:
  sub:                    "77f4ba88-2081-708c-a806-578cb1ac5752"  ← OIDC公開ID(不変)
  email:                  "user@example.com"
  email_verified:         true
  phone_number:           "+819012345678"
  phone_number_verified:  true

Identities:  ← リンク済み外部IdP一覧
  [
    {
      "providerName": "Google",
      "userId": "1234567890",
      "primary": false
    },
    {
      "providerName": "docomo",
      "userId": "dac_xxxxxxxx",
      "primary": false
    },
    {
      "providerName": "YahooJapan",
      "userId": "yj_xxxxxxxx",
      "primary": false
    },
    {
      "providerName": "au-wrapper",
      "userId": "au_opaque_xxxxxxxx",
      "primary": false
    }
  ]

Cognito 内部のインデックス構造(イメージ)

[ユーザーテーブル]
cognito:username (PK)  | sub            | email           | phone
13b99ebb-...           | 77f4ba88-...   | user@example... | +8190...

[エイリアスインデックス]
user@example.com       → 13b99ebb-...
+819012345678          → 13b99ebb-...

[外部IdPインデックス]
Google:1234567890       → 13b99ebb-...
docomo:dac_xxxxxxxx     → 13b99ebb-...
YahooJapan:yj_xxxxxxxx  → 13b99ebb-...
au-wrapper:au_opaque_xx → 13b99ebb-...

どのログイン方法でも、同じ cognito:username に収束する。 これがCognitoのアカウント統合の全体像。

IDトークンの変わる部分・変わらない部分

// どのIdPでログインしても固定
{
  "sub": "77f4ba88-...",               // 常に同じ
  "cognito:username": "13b99ebb-...",  // 常に同じ
  "email": "user@example.com"          // 常に同じ
}

// ログイン方法で変わる
{
  "identities": "[{\"providerName\":\"Google\",...}]"  // Google経由
  "identities": "[{\"providerName\":\"au-wrapper\",...}]"  // au経由
  "identities": null  // メール+パスワード
}

identities クレームを見れば「今回どの方法でログインしたか」がわかるが、アプリ側のロジックでこれを気にする必要は基本ない。cognito:username で処理すれば入口の違いは吸収される。

identities を見る用途があるとすれば:


sub と cognito:username の違い(重要)

この設計で最もハマりやすいポイント。詳しくは前回の記事に書いたが、要点だけ再掲する。

{
  "sub": "77f4ba88-...",               // OIDC標準の不変ID。外部公開用
  "cognito:username": "13b99ebb-..."   // Cognito Admin APIの識別子。内部処理用
}

subAdminGetUser を呼ぶと UserNotFoundException になる。


Cognito 統合アーキテクチャ

                    ┌───────────────────────────────────┐
                    │         Cognito User Pool          │
                    │                                    │
  ドコモ OIDC ──────→ 外部IdP (OIDC)       ─────┐        │
                    │                           │        │
  Yahoo!/SB OIDC ──→ 外部IdP (OIDC)       ─────┤        │
                    │                           ↓        │
  au OpenID 2.0 ──→ OIDCラッパ(Lambda) ──→ 外部IdP ───→  │
                    │                           ↑        │
  Google OIDC ─────→ 外部IdP (Google)      ─────┤        │
                    │                           │        │
  メール+PW ────────→ ネイティブ            ─────┘        │
                    │                     ↓              │
                    │               PreSignUp Lambda      │
                    │               (自動リンク)           │
                    └───────────────────────────────────┘

Cognito IdP 登録

# ドコモ
resource "aws_cognito_identity_provider" "docomo" {
  user_pool_id  = aws_cognito_user_pool.main.id
  provider_name = "docomo"
  provider_type = "OIDC"

  provider_details = {
    oidc_issuer       = "https://conf.uw.docomo.ne.jp/"
    client_id         = var.docomo_client_id
    client_secret     = var.docomo_client_secret
    authorize_scopes  = "openid email phone"
  }

  attribute_mapping = {
    email        = "email"
    phone_number = "phone_number"
    username     = "sub"
  }
}

# ソフトバンク (Yahoo! JAPAN)
resource "aws_cognito_identity_provider" "yahoo_japan" {
  user_pool_id  = aws_cognito_user_pool.main.id
  provider_name = "YahooJapan"
  provider_type = "OIDC"

  provider_details = {
    oidc_issuer       = "https://auth.login.yahoo.co.jp/yconnect/v2"
    client_id         = var.yahoo_client_id
    client_secret     = var.yahoo_client_secret
    authorize_scopes  = "openid email profile"
  }

  attribute_mapping = {
    email    = "email"
    username = "sub"
  }
}

# au (OIDCラッパ経由)
resource "aws_cognito_identity_provider" "au" {
  user_pool_id  = aws_cognito_user_pool.main.id
  provider_name = "au-wrapper"
  provider_type = "OIDC"

  provider_details = {
    oidc_issuer       = "https://auth.rikuka.dev/au-wrapper"
    client_id         = "au-internal"
    client_secret     = var.au_wrapper_secret
    authorize_scopes  = "openid"
  }

  attribute_mapping = {
    username = "sub"  # auのopaque ID
  }
}

PreSignUp Lambda: 自動リンク戦略

新規ユーザーが外部IdPで来たとき、既存のネイティブアカウントと自動でリンクする。

func preSignUp(event events.CognitoEventUserPoolsPreSignUp) {
    triggerSource := event.TriggerSource
    
    // 外部IdP経由のサインアップのみ処理
    if triggerSource != "PreSignUp_ExternalProvider" {
        return // ネイティブサインアップはスルー
    }
    
    provider := getProviderName(event)
    
    switch provider {
    case "docomo":
        // 電話番号で既存ユーザーを検索 → リンク
        phone := event.Request.UserAttributes["phone_number"]
        if phone != "" {
            linkByAttribute("phone_number", phone, event)
        }
        
    case "YahooJapan", "Google":
        // メールで既存ユーザーを検索 → リンク
        email := event.Request.UserAttributes["email"]
        if email != "" {
            linkByAttribute("email", email, event)
        }
        
    case "au-wrapper":
        // 電話番号もメールも取れない → 自動リンク不可
        // 新規アカウント作成。後からUI上で手動連携
    }
}

func linkByAttribute(attr, value string, event) {
    // 同じ属性値の既存ユーザーを検索
    users := cognitoClient.ListUsers(Filter: attr + " = \"" + value + "\"")
    
    if len(users) > 0 {
        // 既存アカウントにリンク
        cognitoClient.AdminLinkProviderForUser(
            DestinationUser: users[0].Username,
            SourceUser: {
                ProviderName: event.UserName のプロバイダー部分,
                ProviderAttributeValue: event.UserName のユーザーID部分,
            },
        )
    }
}

自動リンクの判断基準

IdPマッチングキー自動リンクリスク
Googleメールアドレス✅ 可Googleがメール検証済みなので安全
ドコモ電話番号✅ 可キャリアが電話番号を検証済み。ただし番号再割当てリスクあり
Yahoo!/SBメールアドレス✅ 可Yahoo!がメール検証済みなので安全
auなし❌ 不可opaque IDのみ。手動連携が必要

既存ユーザーへのIdP連携追加

新規登録より、既存ユーザーが後からキャリアを連携する方が自然なフロー。

既存ユーザー(メール+パスワードで登録済み)
  → 設定画面: 「auアカウントを連携する」
  → au でログイン → opaque ID 取得
  → AdminLinkProviderForUser で既存アカウントにリンク
  → 次回から au でワンタップログイン可能
// POST /api/link/au
func LinkAu(c *gin.Context) {
    currentUser := getCurrentUser(c)  // ログイン中のユーザー
    auOpaqueID := c.Get("au_opaque_id")
    
    cognito.AdminLinkProviderForUser(
        DestinationUser: currentUser.CognitoUsername,
        SourceUser: {
            ProviderName:           "au-wrapper",
            ProviderAttributeValue: auOpaqueID,
        },
    )
}

rellf-auth に既にある /api/link/google と同じパターン。


au の opaque ID の注意点

auのopaque IDは安定した永続IDではない

つまり opaque ID は「au IDのセッション的な識別子」であって、「この電話番号の持ち主」の永続的な識別子ではない。

リカバリ設計

通常ログイン:
  au でログイン → opaque ID "aaa" → アカウント特定 → OK

opaque ID が変わった場合:
  au でログイン → opaque ID "bbb" → 見つからない
  → 「このauアカウントは未連携です。メールアドレスでログインしてください」
  → メール+パスワードでログイン → 設定画面から au 再連携

メールか電話番号を1つ持っておけば、opaque IDが変わってもリカバリできる。 au連携はあくまで「便利なショートカット」として位置づけ、アカウントの軸にはしない。


SECRET_HASH の計算(Confidentialクライアントの場合)

Cognitoクライアントにシークレットが設定されている場合、API呼び出しにSECRET_HASHを付ける必要がある。

SECRET_HASH = Base64(HMAC-SHA256(client_secret, USERNAME + client_id))

ここで USERNAMEInitiateAuth に渡す値そのもの

メールでログイン    → computeSecretHash("user@example.com")
UUIDでログイン     → computeSecretHash("13b99ebb-...")

cognito:username ではなく、リクエストに渡す値で計算する。 同じユーザーでもログイン時の識別子が変われば SECRET_HASH も変わる。

SPAやモバイルアプリ(publicクライアント)ではシークレットなし + PKCEを使うので、SECRET_HASHの問題は発生しない。


実装の優先順位

優先度キャリア理由
1ドコモOIDC対応済み、電話番号取得可、dアカウント9000万+
2ソフトバンクYahoo! JAPAN OIDC経由、セルフサービスで開発可能
3auOpenID 2.0ラッパ必要、属性ほぼなし、契約必要

auはOIDCに移行するまで見送るのも現実的な判断。opaque IDしか取れないなら、ユーザーにとっても「auでログイン → でもメール入力して」みたいなUXになり、メリットが薄い。


まとめ

Cognitoの AdminLinkProviderForUser を使えば、複数のIdPを1つのアカウントに統合できる。ただし:

  1. 自動リンクにはマッチングキーが必要 — メールか電話番号。auのように何も取れないIdPは手動連携のみ
  2. sub と cognito:username は別物 — Admin APIには cognito:username を使う
  3. SECRET_HASH はリクエストに渡すUSERNAMEで計算 — cognito:username ではない
  4. opaque ID に依存しすぎない — リカバリ手段としてメールか電話番号を確保する
  5. 既存ユーザーへの連携追加が先、新規登録の入口は後 — リスクが小さく、UXも自然
← Back to all posts