AWS SaaSマルチテナントVol2|B2B SSO・Verified Permissions・M2M

目次

1. この記事について

SaaS Vol2 全体アーキテクチャ — エンタープライズ認証連携概観
図1: SaaS Vol2 全体アーキテクチャ — フェデレーション・認可・M2Mの全体像
📌 Vol1(テナント分離・課金・プロビジョニング編)との関係

  • Vol1は「テナント分離(Silo/Pool/Bridge)・セルベース・従量計測・オンボーディング」というSaaS基盤を扱いました。
  • 本Vol2は、その基盤の上でエンタープライズ顧客を獲得するための認証連携と認可の高度化に踏み込みます。
  • 守備範囲: B2Bフェデレーション(SAML/OIDC)・ユーザー同期(SCIMの現実解)・passkey・Amazon Verified Permissions/Cedarによるきめ細かい認可・M2M認証。

▶ Vol1: テナント分離・セルベース・従量計測・オンボーディング編を読む

1-1. 本記事のゴール

この記事を読み終えると、以下の設計判断軸を持ち帰ることができます。

B2Bフェデレーション(§3)

テナントごとに異なるSAML/OIDC IdP(Okta・Azure AD・Ping Identity等)をAmazon Cognitoで収容し、エンタープライズ顧客のSSOを実現する構成パターンを理解できます。pool-per-tenant方式と単一プール+複数IdP方式のトレードオフ、テナント越境アクセスの防止設計、SAML属性マッピングによるテナントIDクレームのJWT埋め込みまでを扱います。

SCIMの現実解(§4)

CognitoがSCIM 2.0インバウンドプロビジョニングをネイティブに備えないという重要な事実を前提に、JIT(Just-In-Time)プロビジョニング・カスタムサーバーレスSCIMエンドポイント・IAM Identity Center前段の3つのアプローチを比較し、要件に応じた選定基準を示します。

きめ細かい認可(§5)

Amazon Verified Permissions(Cedar言語)でテナント/ロール/リソース/コンテキスト境界をポリシーとして表現し、Vol1で扱ったIAM ABACと組み合わせる2層認可設計を理解できます。IsAuthorized API呼び出しパターンと、マルチテナントでのpolicy store分離方法も扱います。

M2M認証(§6)

Cognitoのclient_credentials grant(RFC 6749)によるサービス間認証・SaaS API公開時のresource server/scope設計・API Gatewayオーソライザーとの組み合わせを実装パターンで整理します。M2Mティア課金(2024年導入)の最新動向も含みます。

トークン戦略(§7)

per-tenant失効(テナント契約終了時の一括ログアウト)・refresh tokenローテーション・JWT署名/aud/iss/クレーム検証パイプラインを、本番運用で必要な粒度で解説します。

これらを通じて「動けばOK」ではなく、テナント境界を本番で守りながらエンタープライズ顧客のSSO/認可/M2M要件に対応できる設計の全体像を掴むことが目標地点です。

1-2. 読者像

想定読者は、Vol1でテナント分離基盤(Silo/Pool/Bridge)を構築済みで、次のステージとしてエンタープライズ顧客のセキュリティ要件に直面しているSaaS開発者・アーキテクトです。

このような経験がある方に特に有益です

  • 商談で「自社Active Directoryと連携できるか」「SCIM自動プロビジョニングはあるか」「SOC2対応の認可ログを出せるか」と聞かれた経験がある
  • Cognitoで外部IdPを設定しようとして、テナントごとの分離設計(同一User Poolに複数のSAML IdPをどう収容するか)に詰まった
  • IAMポリシーでは表現しきれない「テナントAのreader権限ユーザーはドキュメントXを閲覧できるが、テナントBはできない」という認可ロジックをアプリ層に書いており、コードが肥大化している
  • マイクロサービス間・外部パートナーAPIとのM2M連携でトークン設計(scope/audience/有効期限)に迷っている

前提知識

以下の知識があることを前提とします。一から解説するチュートリアルではなく、設計判断と詰まりポイントに集中した実戦ガイドです。

  • Amazon Cognitoの基本操作(User Pool作成・アプリクライアント設定・ホストUI)
  • IAMポリシー・ロール・ABAC(タグベースアクセス制御)の基礎
  • OAuth 2.0 / OIDC / SAML 2.0の概念(authorization_code flow・JWT構造)
  • API Gatewayのオーソライザー設定
  • AWS CDK(TypeScript)またはTerraformの読み書き

AWS認定ソリューションアーキテクト-プロフェッショナル相当の知識があれば、本記事の設計判断を迷わず読み進められます。

1-3. なぜ今これを書くか

SaaSビジネスの成長過程で、エンタープライズ顧客の獲得は大きな転換点となります。大企業との契約は単価が高い一方で、SSOとSCIM自動プロビジョニングを契約の前提条件とするケースが増えています。セキュリティ審査でSSOが必須要件として挙がり、対応できないために商談を失うケースは珍しくありません。この「認証連携・認可レイヤー」の実装難度が、SaaSの成長律速要因になりやすいのです。

AWSのマネージドサービス群は2023〜2025年にかけて大きく進化しました。

  • Amazon Verified Permissions(Cedar): 2023年6月GA。Cedar言語でアプリ層の認可ポリシーを宣言的に記述できるようになり、アプリコードに埋め込まれた認可ロジックをポリシーとして外部化できる
  • Cognito passkey/WebAuthn: 2024年11月、Lite/Essentials/Plusの新ティアモデルとともにGA。Essentials以上でpasskey(FIDO2)によるパスワードレス認証が利用可能
  • M2M課金体系の変更(2024年): M2M専用ティアの導入とper-token-request課金への移行により、従来のper-app-client月額固定費が廃止

一方で、CognitoがSCIM 2.0をネイティブに備えない(SCIMをネイティブ対応するのはIAM Identity CenterであってCognitoではない)という基本事実、Cedarのスキーマバージョン管理の複雑さ、M2M課金変更の詳細など、公式ドキュメントを読んだだけでは気づきにくい落とし穴も多くあります。

Vol1(テナント分離・課金・オンボーディング基盤)では扱わなかった「認証連携・認可レイヤー」の本番ではまりどころを補完するのが本記事の目的です。

1-4. この記事の読み方

本記事は独立した実装テーマごとにセクションが完結する構成です。すべてを順番に読む必要はなく、現在の課題に直接関係するセクションから読み始めることができます。

課題読むべきセクション
エンタープライズ顧客のSSO対応(SAML/OIDC)§2 → §3
ユーザー自動同期(SCIMプロビジョニング)§2 → §4
アプリ層の細かい権限制御(Cedar/AVP)§2 → §5
マイクロサービス/外部APIとのM2M連携§2 → §6
トークン失効・refresh token運用§7
全体設計をまず理解したい§2(全体アーキテクチャ)→ 各セクション

なお、§2の「前提・全体アーキテクチャ」は各セクションを読む前の共通土台です。初読の方は§2を先に確認することを推奨します。

📌 Vol2の対象外(Vol3以降で扱う予定のテーマ)

  • CloudTrail/CloudWatch Logsを使ったper-tenant認証監査ログ設計
  • GuardDuty・Security Hubとの連携によるテナント異常検知
  • AWS WAFによるテナント別レートリミット・bot対策

2. 前提・全体アーキテクチャと東京リージョン対応

B2B federation per-tenant SAML/OIDC IdP フロー
図2: B2Bフェデレーション — テナント別IdP接続のフロー

2-1. 前提環境

本記事は以下の環境が整っていることを前提とします。

テナント基盤(Vol1相当)

  • Silo/Pool/Bridgeのいずれかのテナント分離モデルが構築済みであること
  • テナントIDがIAMタグ(例: TenantID: tenant-abc)・Cognitoカスタム属性(custom:tenant_id)として付与されていること
  • テナントごとのオンボーディングフロー(Cognito User Pool登録・DynamoDBテナント台帳)が存在すること

Amazon Cognito

  • Amazon Cognito User Pool が作成済みで、アプリクライアントが設定済みであること
  • Cognitoティア: 本記事のpasskey機能には Essentials ティア以上 が必要(2024年11月以降の新規User PoolはデフォルトでEssentialsティアが選択される)
  • ホストされたUI(Hosted UI)またはカスタムUIでの認証フローが動作していること

AWSアカウントと権限

  • Cognito・Verified Permissions・IAM Identity Center(SCIMを使う場合)・Lambda・API Gateway・CloudFormation/CDKへのフル管理権限
  • AWS Organizations配下でマルチアカウント構成の場合は、IAM Identity CenterをOrganizationレベルで有効化する権限も必要

テスト用外部IdP

SAMLフェデレーションの検証には以下のいずれかを推奨します。

  • Okta Developer Account(無料): SAML/OIDCの両プロトコルを素早く設定できる
  • Azure AD評価テナント(無料90日): 実際のEntraID環境に近い検証が可能
  • KeycloakをローカルDockerで起動: 完全にオフラインで検証でき、証明書ローテーションのシミュレーションも容易

開発環境

  • AWS CDK v2(TypeScript)またはTerraform v1.5以降(スニペットはCDK TypeScriptを使用)
  • Node.js 20以降・Python 3.11以降(Lambda関数の実装例で使用)
  • AWS CLI v2、設定済みAWSクレデンシャル(aws configure または IAM Identity Centerのsso-session)
  • jq(CLIでJWTクレームを確認する際に使用)

上記の前提が揃っていない状態で§3以降の実装を進めると、IdPとのメタデータ設定ミスやテナントID属性の欠落など、デバッグコストが高いトラブルに直面することがあります。前提環境を事前に確認してから進めることを強く推奨します。

特にCognitoのティア確認(Lite vs Essentials)は、aws cognito-idp describe-user-pool --user-pool-id <id>UserPoolTier フィールドで即座に確認できます。

2-2. 使用サービスと東京リージョン(ap-northeast-1)対応

本記事で使用するサービスと東京リージョン(ap-northeast-1)での提供状況を整理します。

サービス/機能役割東京(ap-northeast-1)
Amazon Cognito User Pools認証・フェデレーション・トークン発行GA
Amazon Cognito passkey/WebAuthnパスワードレス認証(Essentialsティア以上が必須)GA
Amazon Verified Permissions(Cedar)アプリ層のきめ細かい認可GA
Cognito M2M(client_credentials grant)サービス間認証GA
AWS IAM Identity CenterSCIMインバウンドプロビジョニング前段GA
Amazon API GatewayM2M API公開・CognitoオーソライザーGA
AWS LambdaカスタムSCIMエンドポイント・トリガー処理GA

2025年6月時点で、上記全サービスが東京リージョン(ap-northeast-1)でGAです。

Cognitoティアとpasskey

Cognitoはサービスプランとして「Lite」「Essentials」「Plus」の3ティアが存在します(2024年11月GA)。passkey/WebAuthnはEssentials以上で利用可能です。既存User Pool(2024年11月以前に作成)はデフォルトでLite扱いですが、コンソールまたはUpdateUserPool API(UserPoolTierパラメータ/CLIは--user-pool-tier)でいつでもティアを変更できます(一部の機能を有効化したまま下位ティアへ移行する場合は当該機能の無効化が必要)。参照: Amazon Cognito developer guide — Cognito feature tiers

Amazon Verified Permissionsのバージョン管理

Amazon Verified Permissionsは2023年6月にGA、Cedar言語バージョン2.xへのアップデートが継続中です。Cedar言語のバージョンとVerified Permissionsの対応はCedarVersionスキーマフィールドで管理されます。使用する際は必ずAWS公式ドキュメントで対応バージョンを確認してください(参考: Amazon Verified Permissions User Guide)。

M2Mティア課金(2024年〜)

2024年にCognitoのM2M課金体系が変更され、per-app-client月額固定費が廃止され、per-token-requestベースに移行しました。高頻度のM2Mトークン取得を設計する場合はトークンキャッシュ(有効期限内再利用)が重要です(詳細は§6-2)。参照: Amazon Cognito pricing

AWS IAM Identity Center(SCIMを使用する場合のみ)

IAM Identity CenterはOrganization単位またはスタンドアロンモードで有効化します。既存のCognito User Poolとは別の認証レイヤーとなるため、SCIMを採用する場合はアーキテクチャの二重管理(IdCとCognito)を許容する設計判断が必要です(詳細は§4)。

2-3. ゴール状態の定義

本記事の各セクションを完走した時点での成果物と動作確認可能な状態を定義します。

認証連携(§3・§4)完走後

  • テナントAのユーザーが企業IdP(SAML/OIDC)でSSOログインでき、CognitoアクセストークンのカスタムクレームにテナントID(custom:tenant_id)が付与される
  • テナントAのIdPで認証したユーザーがテナントBのリソースにアクセスできない(テナント越境が防止されている)
  • JITプロビジョニングによりユーザー初回ログイン時にCognitoユーザーが自動作成され、テナントIDカスタム属性が設定される
  • SCIMを採用した場合: IdPでユーザーをグループに追加すると、当該ユーザーがSaaS側に自動プロビジョニングされる
  • SCIMを採用した場合: IdPでユーザーを無効化/削除すると、SaaS側でも速やかにアクセスが停止される

認可(§5)完走後

  • Cedar policy storeにテナント・ロール(admin/viewer/editor)・リソース種別を表現するスキーマが定義されている
  • IsAuthorized API呼び出しにより「テナントAのadminはリソースXをdeleteできるが、同テナントのviewerはできない」「テナントBのadminはテナントAのリソースにアクセスできない」が正しく判定される
  • policy storeのポリシーがテナント間で混在しない(テナントIDをcontext/entityに含めた境界設計が実装されている)
  • IsAuthorizedの判定結果がCloudWatch Logsに記録され、認可監査ログとして参照できる

M2M(§6)完走後

  • client_credentials grantでCognitoトークンエンドポイント(/oauth2/token)からアクセストークンが取得できる
  • サービスBがサービスAのAPIを、このトークンをAuthorizationヘッダーに載せて呼び出せる
  • 不正なscope(サービスBに許可されていないscope)のトークンがAPI Gatewayのオーソライザーで403 Forbiddenとして拒否される
  • M2Mトークンが有効期限内にキャッシュ再利用されており、Cognitoへの不要なtoken requestが抑制されている

トークン戦略(§7)完走後

  • テナント契約終了時に、対象テナントユーザー全員のrefresh tokenを一括失効(AdminUserGlobalSignOut)できる
  • refresh token rotation が有効化されており、refresh tokenが使い捨てになっている
  • JWT検証レイヤー(署名・aud・iss・exp・custom:tenant_idクレーム)がAPI側ミドルウェアに実装されている

これらのゴール状態はいずれも自動テストで確認できます。§3〜§7では各ゴール状態を検証するためのCLIコマンド・CDKテストスニペット・手動確認手順を合わせて記載します。実装後に必ずこれらの確認ステップを実行し、テナント境界の漏洩がないことを検証してください。

2-4. 全体アーキテクチャ概観

SaaS Vol2 全体アーキテクチャ — エンタープライズ認証連携概観
図1: SaaS Vol2 全体アーキテクチャ — フェデレーション・認可・M2Mの全体像

本Vol2で扱う認証・認可・M2Mのアーキテクチャは、大きくコントロールプレーン(CP)アプリケーションプレーン(AP)の2層に分かれます。

コントロールプレーン(CP)— テナント管理基盤

コントロールプレーンはVol1で構築したテナント管理基盤を指します。テナントのオンボーディング(新規登録・プロビジョニング)・設定管理・課金基盤がここに集約されます。Vol2では、CPにフェデレーション設定管理が加わります。

  • テナント台帳(DynamoDB)にIdPメタデータURL・プロトコル(SAML/OIDC)・テナントドメインを格納
  • 管理APIを経由してCognito User PoolへのIdP設定をIaC(CDK/Terraform)またはAPI経由でプロビジョニング
  • テナント解約時はCP経由でIdP設定削除・ユーザー一括失効・SCIMデプロビジョニングをオーケストレーション

アプリケーションプレーン(AP)— 認証・認可・M2Mの実行基盤

アプリケーションプレーンはエンドユーザー向けのリクエストが流れる層です。本Volで扱う主要コンポーネントは以下の通りです。

  1. フェデレーションレイヤー(§3・§4)
  2. Amazon Cognito User Pools が外部IdP(SAML/OIDC)との認証フローを仲介
  3. ログイン成功時にCognitoが署名付きJWT(IDトークン・アクセストークン・リフレッシュトークン)を発行
  4. JWTのカスタムクレームにテナントID(custom:tenant_id)を含め、AP全体でテナント識別子として使用

  5. 認可レイヤー(§5)

  6. API GatewayまたはアプリサーバーがJWT検証後、Amazon Verified PermissionsのIsAuthorized APIを呼び出し
  7. Cedar policy storeでテナント・ロール・リソース・アクションの4次元で認可ポリシーを評価
  8. 評価結果(Allow/Deny)をアプリに返却し、APIがビジネスロジックを実行またはエラーを返す

  9. M2M認証レイヤー(§6)

  10. マイクロサービス間やSaaS APIへの外部パートナーアクセスは、Cognitoのclient_credentials grantを使用
  11. 各サービス/パートナーにApp Client(クライアントID/シークレット)を発行し、resource server scopeで最小権限
  12. API GatewayのCognitoオーソライザーでトークン検証し、対象scopeのトークンのみ通過させる

この3層構造(フェデレーション → 認可 → M2M)が本Volの基本設計です。各レイヤーは独立して導入できるため、現在のシステム状況に応じて優先度を決めて段階的に実装できます。

Vol1との接続点

Vol1で構築したテナント分離基盤(テナントID付きIAMロール・テナント分離データアクセス)は、本Volのトークン設計と密接に連携します。具体的には、CognitoがJWT発行時にカスタムクレーム(custom:tenant_id)を付与し、そのクレームをIAM ABACの条件キー(aws:RequestTag/TenantID)とVol2のCedar認可ポリシーのcontext双方に連携させることで、インフラ層とアプリ層の2層でテナント境界を守る構造になります。既存のVol1設計を変更せずに本Volの認証・認可レイヤーを追加できる点が、このアーキテクチャの強みです。


3. B2Bエンタープライズ フェデレーション — per-tenant SAML/OIDC

3-1. テナント別IdP接続の設計

エンタープライズ顧客(大企業・公共機関)はOkta、Microsoft EntraID、Google Workspace、独自ADFSなど各社固有のIdPを既に保有しており、SaaSへのログインにはそのIdPを使ったSSOを必須条件として要求するケースが増えている。Amazon Cognito User Poolsは外部IdPとのフェデレーションをSAML 2.0とOIDCの両プロトコルでサポートしており、この要件に対応できる。

マルチテナントSaaSにおける設計上の第一の選択は、「テナントごとにUser Poolを分けるか(Pool-per-tenant)」それとも「単一のUser Poolに複数のIdPを登録するか(Shared Pool)」です。

観点Pool-per-tenantShared Pool + per-tenant IdP
テナント分離レベルコントロールプレーン完全分離データプレーン共有・IdPで論理分離
スケール上限リージョンあたり最大1,000 UserPool(ソフトリミット)1 UserPoolあたりSAML IdP最大300件
カスタムドメインテナントごとに独立して設定可1ドメインを全テナントで共有
Terraform管理テナントモジュールを繰り返しプロビジョニングIdP追加のみ。UserPool設定は共通
推奨シナリオSilo分離・厳格なコンプライアンス要件Pool共有・テナント数が数十〜数百規模

Vol1のSilo/Pool分離方針と整合させることが重要で、データアクセス層がSilo(テナントごとAWSアカウント)であればPool-per-tenant、Pool分離であればShared Pool + per-tenant IdPが自然な選択となる。

テナントとIdPのルーティング手法

ユーザーがログイン画面に到達したとき、アプリ側がどのIdPに誘導するかの判定方法として次の3パターンがある。

  1. IdP選択UI(Hosted UI): Cognitoのホステッドページが登録済みIdPのボタンを自動表示する。テナント数が少ない(10社以下)場合に手軽だが、スケールしない。

  2. identity_provider クエリパラメータ: アプリがメールドメインをキーにテナントを特定し、認可リクエストに直接IdP名を付与してホステッドUIをバイパスする最も実用的な手法。

GET /oauth2/authorize
?client_id={client_id}
&response_type=code
&identity_provider=TenantA-SAML
&redirect_uri={redirect_uri}

  1. テナント別App Client: テナントごとにApp ClientとCallback URLを分け、それぞれにデフォルトIdPを割り当てる。テナント構成が変わらない安定した環境に向いている。
📌 IdPルーティングの実装選択基準

  • テナント数 < 20: Hosted UI + IdP選択ボタンで十分。実装コスト最小。
  • テナント数 20〜数百: メールドメイン → IdP名のマッピングテーブルをDynamoDB等に保持し、identity_provider クエリパラメータで自動ルーティング。
  • テナント数 数百〜: Pool-per-tenant + テナント設定DBでのルーティング。Terraformモジュール化が前提。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-federation.html


3-2. SP-initiated / IdP-initiated と属性マッピング

SP-initiated フロー(推奨標準パターン)

SAMLにおけるSP-initatedフローは、アプリ(Service Provider = Cognito)側から認証リクエストを起点とする形式で、セキュリティ上の理由から推奨されるパターンです。

ユーザー → アプリ → テナント判定
  → GET /oauth2/authorize?identity_provider=TenantA-SAML
  → Cognito → SAML AuthnRequest → IdP(Okta等)
  → IdPがユーザー認証
  → POST {ACS URL} SAMLResponse
  → Cognito: Assertion検証 + 属性マッピング
  → JWTトークン発行 → アプリ(Callback URL)

Cognito側のエンドポイント:
ACS URL: https://{domain}.auth.{region}.amazoncognito.com/saml2/idpresponse
SP Metadata URL: https://{domain}.auth.{region}.amazoncognito.com/saml2/metadata/{ProviderName}
Entity ID: urn:amazon:cognito:sp:{userPoolId}

IdP-initiated フロー(限定的に使用)

IdPのダッシュボードからアプリアイコンをクリックして直接起動するフローで、CognitoはサポートしていますがRelayStateの設定が必須です。IdP側の「Default RelayState」に以下を設定する(URLエンコード済み)。

https%3A%2F%2F{domain}.auth.{region}.amazoncognito.com%2Foauth2%2FidpResponse

未設定の場合、認証には成功するがアプリへのリダイレクトが行われず、Cognitoのエラー画面で止まる。この詳細は3-3を参照。

SAML属性マッピングの設定

CognitoのSAML IdP設定で「属性マッピング」を定義することで、SAMLアサーション内の属性をCognitoユーザー属性に変換できる。よく使用する標準マッピングを以下に示す。

SAMLアサーション属性(IdP側)Cognito属性用途
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddressemailユーザー識別
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givennamegiven_name名前
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surnamefamily_name
tenantId(IdP側カスタム属性)custom:tenantIdテナント識別子

Terraformでの設定例(Okta SAML):

resource "aws_cognito_identity_provider" "tenant_a_saml" {
  user_pool_id  = aws_cognito_user_pool.main.id
  provider_name = "TenantA-SAML"
  provider_type = "SAML"

  provider_details = {
 MetadataURL = "https://tenanta.okta.com/app/example/sso/saml/metadata"
 IDPSignout  = "true"
  }

  attribute_mapping = {
 email = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
 given_name  = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
 "custom:tenantId" = "tenantId"
  }
}

OIDC外部プロバイダーの設定(EntraID / GoogleWorkspace / Okta)

OIDCプロバイダーの場合、IdPが公開するディスカバリーエンドポイント(/.well-known/openid-configuration)からCognitoが設定を自動取得する。

必要な情報:
Provider name: テナントを識別する一意の名前(例: TenantB-EntraID
Client ID / Client secret: IdP側で発行したOAuthアプリの認証情報
Issuer URL: EntraIDなら https://login.microsoftonline.com/{tenantId}/v2.0
Authorize scopes: openid email profile

Terraformでの設定例(Microsoft EntraID):

resource "aws_cognito_identity_provider" "tenant_b_entraid" {
  user_pool_id  = aws_cognito_user_pool.main.id
  provider_name = "TenantB-EntraID"
  provider_type = "OIDC"

  provider_details = {
 client_id  = var.entraid_client_id
 client_secret = var.entraid_client_secret
 attributes_request_method = "GET"
 oidc_issuer= "https://login.microsoftonline.com/${var.azure_tenant_id}/v2.0"
 authorize_scopes = "openid email profile"
  }

  attribute_mapping = {
 email = "email"
 "custom:tenantId" = "tid"
  }
}

Google Workspaceの場合は oidc_issuer = "https://accounts.google.com" で、hd(hosted domain)クレームでテナントドメインを検証できる。

JWTクレームへのカスタム属性反映

custom:tenantId 等のカスタム属性を、CognitoがデフォルトでJWTに含めるわけではない。Pre Token Generation Lambda(V2トリガー)を使うことで、IDトークンとアクセストークン双方にカスタムクレームを追加できる(V2はアクセストークンのカスタムクレームに対応した点が重要)。

def handler(event, context):
 tenant_id = event["request"]["userAttributes"].get("custom:tenantId", "")
 event["response"]["claimsAndScopeOverrideDetails"] = {
  "accessTokenGeneration": {
"claimsToAddOrOverride": {
 "tenantId": tenant_id
}
  },
  "idTokenGeneration": {
"claimsToAddOrOverride": {
 "tenantId": tenant_id
}
  }
 }
 return event

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html


3-3. 詰まりポイント

詰まり①: テナント越境ログインの防止

「テナントAのユーザーがテナントBのIdPで認証を通過してしまう」という横断を防ぐには、Pre Authentication Lambda トリガーでIdPとテナントの組み合わせを検証する。

def handler(event, context):
 # フェデレーションユーザーのuserName形式: "TenantA-SAML_john@tenanta.com"
 username = event.get("userName", "")
 idp_prefix = username.split("_")[0] if "_" in username else ""

 tenant_id = event["request"]["userAttributes"].get("custom:tenantId", "")

 allowed_mapping = {
  "TenantA-SAML": "tenantA",
  "TenantB-EntraID": "tenantB",
 }

 if allowed_mapping.get(idp_prefix) != tenant_id:
  raise Exception("Tenant/IdP mismatch: unauthorized cross-tenant authentication")

 return event

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-authentication.html

詰まり②: SAML署名証明書の失効

OktaやEntraIDのSAML署名証明書は3〜5年で失効します。証明書失効日以降、テナント全員がSSOできなくなるという致命的な障害に直結するため、事前の運用設計が必須です。

方式更新方法リスク
MetadataURL方式Cognito側が定期取得(最長24時間キャッシュ)キャッシュ期間のタイムラグがある
MetadataFile手動アップロード管理者が手動でXMLをアップロード更新忘れで突然SSOが停止

推奨対策:
MetadataURL方式を優先採用する(IdPのメタデータURLをCognito設定に直接指定)。
– IdP管理コンソールで証明書有効期限を確認し、失効60日前にEventBridgeアラートを設定する。
– 証明書更新後は aws cognito-idp describe-identity-provider で現在のフィンガープリントを検証する。

詰まり③: SP Metadataの更新が必要なケース

Cognito側の設定変更によってACS URLやEntity IDが変わった場合、IdP(Okta/EntraID)側のSP Metadata登録を更新しなければSSOが断絶する。

変更が生じるケース:
– Cognitoカスタムドメインの変更・削除
– UserPool削除 + 再作成(開発/ステージング環境のリセット時に頻発)
provider_name の変更

SP Metadataは次のURLから最新版を取得できる。IdP側に再アップロードするか、メタデータURLとして設定する。

https://{your-domain}.auth.{region}.amazoncognito.com/saml2/metadata/{ProviderName}
⚠️ 開発環境リセット時の注意

  • Terraformで terraform destroyterraform apply するとUserPool IDが変わりEntity IDが変化する。
  • IdP側のSP設定(Oktaのアプリ設定等)を毎回更新する必要があるため、開発環境では単一の永続的なUserPoolを使い回すか、SP MetadataのURL更新手順をRunbook化しておくこと。

詰まり④: RelayState設定漏れ(IdP-initiated SSO)

IdP-initiated SSOを有効にする場合、IdP管理コンソールの「Default Relay State」設定が空のままだとSSOが実質機能しません。Cognitoが期待するRelayStateの値は次のとおりです。

https://{your-domain}.auth.{region}.amazoncognito.com/oauth2/idpResponse

URLエンコード済みで設定する場合:

https%3A%2F%2F{your-domain}.auth.{region}.amazoncognito.com%2Foauth2%2FidpResponse

なお、OAuth2の state パラメータとRelayStateは別概念です。state はCSRF対策用にアプリが生成するランダム値であり、SAMLのRelayStateとは混同しないでください。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-saml-idp-troubleshooting.html


4. エンタープライズユーザー同期 — SCIMの現実解

4-1. 重要な前提: Cognitoは SCIM 2.0 を内蔵しない

Amazon Cognito User Pools は SCIM 2.0 サーバー(インバウンド自動プロビジョニング)をネイティブに備えていません。

エンタープライズ顧客から「Okta や Entra ID から SCIM でユーザーをプロビジョニングしたい」と要望された際、「Cognito に SCIM エンドポイントを向ければよい」と誤解されることがあります。しかし Cognito にはインバウンド SCIM を受け取るエンドポイントは存在しません。

サービスSCIM 2.0 サポート
Amazon Cognito User Poolsネイティブ未対応(独自 Admin API で代替)
AWS IAM Identity Centerネイティブ対応(自動プロビジョニング / デプロビジョニング)

SCIM 2.0 をネイティブに受け取れる AWS サービスは AWS IAM Identity Center(旧 AWS SSO) です。Cognito でエンタープライズ向けユーザー同期を実現するには、§4-2 で示す 3 パターンのいずれかを選択する必要があります。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-federation.html

4-2. 対応3パターンと選定

エンタープライズ顧客の IdP(Okta / Microsoft Entra ID / OneLogin 等)からユーザーを同期する方法は、要件に応じた 3 パターンへ集約されます。

SCIM 自動プロビジョニング シーケンス
図3: エンタープライズユーザー同期(SCIM対応)のシーケンス

パターンA: JIT プロビジョニング(Just-In-Time)

SAML 2.0 または OIDC フェデレーションの初回ログイン時に、Cognito がユーザーを自動作成するパターンです。

IdP (Okta/Entra ID) → SAML/OIDC → Cognito User Pool → JIT でユーザー作成

Cognito の「属性マッピング」設定で、SAML Assertion / ID Token の属性(emailgiven_namecustom:tenant_id 等)を Cognito ユーザー属性へ自動マッピングします。ユーザーは初回ログイン時にのみプロビジョニングされ、以降は IdP のセッションを通じて認証されます。

制約: ユーザーが一度もログインしない場合(招待送付後に未ログインで退職等)は Cognito にユーザーが存在しないため、管理者による API 操作(ユーザー検索・無効化等)に支障が生じます。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-saml-idp.html


パターンB: カスタム Serverless SCIM エンドポイント

API Gateway + Lambda + Cognito Admin API で SCIM 2.0 仕様のエンドポイントを自前実装するパターンです。IdP 側で SCIM プロビジョニング先として設定できます。

IdP (Okta/Entra ID)
  → SCIM 2.0 (HTTPS POST/PUT/PATCH/DELETE)
  → API Gateway
  → Lambda (SCIM handler)
  → Cognito AdminCreateUser / AdminUpdateUserAttributes / AdminDisableUser / AdminDeleteUser

Lambda では以下の SCIM 操作を Cognito Admin API へ変換します。

SCIM 操作Cognito Admin API
POST /Users(作成)AdminCreateUser
PUT /Users/{id}(更新)AdminUpdateUserAttributes
PATCH /Users/{id}(無効化)AdminDisableUser
DELETE /Users/{id}(削除)AdminDeleteUser

SCIM の externalId と Cognito の username を DynamoDB で対応付けておくことで、IdP のユーザー ID と Cognito ユーザー間の一意マッピングを維持します。

注意点: SCIM 2.0 仕様(RFC 7643/7644)への準拠実装は工数が多く、IdP ごとに微妙な実装差異があります。Okta では PUT ではなく PATCH を主に使用するなど、テスト工数を見込む必要があります。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pools-API-operations.html


パターンC: AWS IAM Identity Center を前段に配置

SCIM をネイティブ対応する IAM Identity Center を前段に置き、ユーザー同期後にアプリケーションへ OIDC フェデレートするパターンです。

IdP (Okta/Entra ID)
  → SCIM 2.0 → IAM Identity Center(自動プロビジョニング)
  → OIDC/SAML → Cognito User Pool(またはアプリ直接)

IAM Identity Center は組み込みの SCIM 2.0 エンドポイントを持ち、Okta・Entra ID・OneLogin の標準 SCIM コネクタで即時連携できます。プロビジョニングされたユーザーは IAM Identity Center 側で管理され、グループ割り当てによるアプリアクセス制御も可能です。

適用場面: すでに AWS Organizations を利用しており IAM Identity Center を導入済みの場合、または大規模エンタープライズで複数 AWS アカウント・複数アプリケーションへの SSO 統合を一元管理したい場合に最適です。

source_url: https://docs.aws.amazon.com/singlesignon/latest/userguide/provision-automatically.html


3パターンの比較

比較軸A: JITB: カスタム SCIMC: Identity Center 前段
実装コスト低(設定のみ)高(Lambda 実装・テスト)中(IdP 設定 + OIDC 連携)
保守性高(コードなし)低(SCIM 仕様対応が必要)高(マネージドサービス)
既存 IdP 親和性中(初回ログイン必須)高(標準 SCIM 対応)高(標準 SCIM 対応)
ユーザー管理の即時性低(ログイン前は Cognito 未登録)高(即時プロビジョニング)高(即時プロビジョニング)
大規模テナント対応中(数百〜数千ユーザー)高(スケーラブル)高(組織全体統合)
推奨シナリオ小〜中規模・低コスト優先Cognito 直接統合・要件特化大企業・複数アプリ SSO 統合

選定指針: スタートアップや中規模 SaaS ではパターン A が最も低コストです。エンタープライズ顧客が「ユーザー登録後すぐに API 経由でアクセス制御したい」「退職者を即時無効化したい」と要求する場合はパターン B またはパターン C を選択します。AWS Organizations を既に利用している企業ではパターン C が運用コストを最小化します。

4-3. デプロビジョニング(退社・契約終了)

ユーザーの退社や SaaS 契約終了時のライフサイクル管理は、エンタープライズ顧客のセキュリティ要件で最も厳しく審査される領域の一つです。

ユーザー無効化・削除のライフサイクル

Cognito ではユーザーの「無効化」と「削除」を使い分けます。

操作API動作推奨タイミング
無効化AdminDisableUserログイン不可(ユーザーデータ保持)退職直後・調査中
グローバルサインアウトAdminUserGlobalSignOut全セッションのトークン失効無効化と同時実行
削除AdminDeleteUserユーザーデータ完全消去保持期間(例: 30日)経過後

即時無効化のベストプラクティス: 無効化と同時に AdminUserGlobalSignOut を呼び出すことで、アクティブなアクセストークン・リフレッシュトークンをすべて失効させます。アクセストークンのデフォルト有効期限(最大 60 分)が切れるまでは有効なため、即時遮断が必要な場合はこの操作が必須です。

import boto3

cognito = boto3.client('cognito-idp')

def offboard_user(user_pool_id: str, username: str) -> None:
 cognito.admin_user_global_sign_out(
  UserPoolId=user_pool_id,
  Username=username
 )
 cognito.admin_disable_user(
  UserPoolId=user_pool_id,
  Username=username
 )

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-account-recovery.html

監査要件への対応(CloudTrail + Cognito Admin API)

Cognito の管理操作(AdminCreateUserAdminDisableUserAdminDeleteUser 等)はすべて AWS CloudTrail に記録されます。監査証跡として以下を活用できます。

  • CloudTrail Event History: 誰がいつユーザーを無効化・削除したかを証跡として保存
  • CloudWatch Logs Insights: 特定ユーザーへの管理操作履歴をクエリ
  • Amazon S3 + Athena: 長期保存(7 年等)と大規模クエリに対応
-- CloudTrail ログ (Athena) でユーザー操作を追跡するクエリ例
SELECT eventtime, useridentity.arn, requestparameters
FROM cloudtrail_logs
WHERE eventsource = 'cognito-idp.amazonaws.com'
  AND eventname IN ('AdminDisableUser', 'AdminDeleteUser', 'AdminCreateUser')
  AND json_extract_scalar(requestparameters, '$.username') = 'target-user@example.com'
ORDER BY eventtime DESC;

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/logging-using-cloudtrail.html

テナント解約時の一括処理パターン

SaaS 契約終了時(テナント全体の解約)では、テナントに属する全ユーザーを一括で処理する必要があります。Cognito の ListUsers API はカスタム属性によるフィルタリングをサポートしているため、custom:tenant_id でテナントユーザーを抽出して一括無効化できます。

def offboard_tenant(user_pool_id: str, tenant_id: str) -> int:
 paginator = cognito.get_paginator('list_users')
 pages = paginator.paginate(
  UserPoolId=user_pool_id,
  Filter=f'custom:tenant_id = "{tenant_id}"'
 )
 count = 0
 for page in pages:
  for user in page['Users']:
offboard_user(user_pool_id, user['Username'])
count += 1
 return count

データ保持ポリシーの考慮: テナント解約後もユーザーデータを一定期間保持する要件(GDPR 忘れられる権利への対応等)がある場合は、削除前に AdminGetUser でユーザー属性を S3 にエクスポートし、監査証跡として保管するパターンを採用します。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/how-to-manage-user-accounts.html


5. きめ細かい認可 — Amazon Verified Permissions / Cedar

Amazon Verified Permissions / Cedar 認可アーキテクチャ
図4: Amazon Verified Permissions / Cedar によるアプリ層認可

5-1. インフラ層ABAC(Vol1)とアプリ層認可(本Vol)の違い

Vol1ではIAM ABACを使い、テナントIDタグによるインフラリソース(DynamoDB・S3バケット等)のアクセス境界を設定しました。しかし「IAM層の認可」だけではアプリケーション内部の機能制御(「テナントAのユーザーBはドキュメントXを編集できるか」等)には対応できません。

担当サービス制御対象典型的な判断軸
インフラ層ABACIAM / SCPAWSリソース(S3オブジェクト、DynamoDBテーブル、Lambda)テナントIDタグ + IAMロール
アプリ層fine-grainedAmazon Verified Permissions(Cedar)アプリ機能 / ドキュメント / API操作ユーザーロール + リソース属性 + テナントコンテキスト

なぜ2層が必要か

IAM ABACはAWSリソースの境界を守ります。一方Cedar/AVPはアプリケーションのビジネスロジック境界(「管理者は他テナントのレポートを参照できない」「承認待ち状態のドキュメントは作成者のみ編集可」等)を守ります。IAM単体ではDynamoDBテーブル内の行レベル・属性レベルの制御ができないため、アプリ層で補完するのが本番SaaSの標準パターンです。

エンタープライズ顧客から「監査可能な権限制御レポートがほしい」と要求された際、IAM単体ではアプリ操作レベルの証跡を表現しにくいですが、Cedar + CloudTrailでAPIレベルの認可判定ログを統一的に残せます(IsAuthorized 呼び出しはCloudTrailに記録されます)。

参照: AWS公式「Amazon Verified Permissions とは

5-2. Cedar ポリシー設計

Cedar の基本構造

Cedar(オープンソース、Apache 2.0ライセンス)はAmazonが開発したポリシー言語でprincipal / action / resource / contextの4要素で構成されます。

// 読み取り許可の例
permit(
  principal in UserGroup::"tenant-acme-admins",
  action in [Action::"Document::read", Action::"Document::update"],
  resource in Folder::"tenant-acme-docs"
);

// テナント越境防止: リソースとプリンシパルのtenantIdが一致する場合のみ許可
permit(
  principal,
  action == Action::"Document::read",
  resource
) when {
  principal.tenantId == resource.tenantId
};

スキーマ定義(SaaS向け)

マルチテナントSaaSではテナントIDをEntityのattributeとして持たせ、全ポリシーの条件に必須参照するのが基本設計です。

{
  "MyNamespace": {
 "entityTypes": {
"User": {
  "shape": {
 "type": "Record",
 "attributes": {
"tenantId": { "type": "String", "required": true },
"roles": { "type": "Set", "element": { "type": "String" } }
 }
  }
},
"Document": {
  "shape": {
 "type": "Record",
 "attributes": {
"tenantId": { "type": "String", "required": true },
"ownerId":  { "type": "String" }
 }
  }
}
 },
 "actions": {
"Document::read": {
  "appliesTo": {
 "principalTypes": ["User"],
 "resourceTypes": ["Document"]
  }
},
"Document::update": {
  "appliesTo": {
 "principalTypes": ["User"],
 "resourceTypes": ["Document"]
  }
}
 }
  }
}

IsAuthorized API 呼び出しパターン

AVPは2種類の認可APIを提供します。

API渡すもの向いている場面
IsAuthorizedEntityデータを自前で組み立てて渡すDBからEntity情報を取得してコンテキストを追加したい場合
IsAuthorizedWithTokenCognitoのJWTトークンをそのまま渡すCognitoとAVPを直結してエンティティ解決を自動化したい場合

参照: Cedar言語リファレンス「Cedar policy language」/ AWS「IsAuthorizedWithToken API

5-3. マルチテナントでの認可分離

Policy Store 設計の選択肢

パターンPolicy Store特徴
テナント別 Policy Storeテナントごとに1つ作成完全な物理分離・テナント削除が容易だが管理コスト増
共有 Policy Store + テナントスコープ全テナントで1つ管理が集中・スケールしやすいが誤混入対策が必要

テナント数が100以下の固定型SaaSは「テナント別Policy Store」が監査上の明瞭さから推奨されます。テナント動的生成(エンタープライズSaaSで多数の中小テナントを扱う)場合は「共有 + テナントスコープ」でtenantIdを全ポリシーのwhen条件に必須化します。

Policy Template の活用(テナントオンボーディング自動化)

テナントごとに同一構造のポリシーを大量生成する場合はPolicy Templateを使い、オンボーディング時にAPI経由で自動生成します。

// Policy Template — principal/resource の placeholder にテナント情報をバインド
permit(
  principal in ?principal,
  action in [Action::"Document::read", Action::"Document::update"],
  resource in ?resource
) when {
  principal.tenantId == resource.tenantId
};
import boto3

avp = boto3.client('verifiedpermissions', region_name='ap-northeast-1')

def provision_tenant_policies(policy_store_id: str, tenant_id: str, template_id: str):
 # テナントオンボーディング時にtemplate instantiation
 avp.create_policy(
  policyStoreId=policy_store_id,
  definition={
'templateLinked': {
 'policyTemplateId': template_id,
 'principal': {
  'entityType': 'UserGroup',
  'entityId': f'tenant-{tenant_id}-users'
 },
 'resource': {
  'entityType': 'Folder',
  'entityId': f'tenant-{tenant_id}-docs'
 }
}
  }
 )

参照: AWS「CreatePolicyFromTemplate


5-4. Verified Permissions + Cognito 連携アーキテクチャ

CognitoとAVPを組み合わせる際の推奨パターンはIdentity Sourceの設定です。

Identity Source 登録手順

  1. AVPのPolicy Storeに対して、Cognito User Poolを「Identity Source」として登録します。
  2. アプリケーションはCognito発行のJWT(アクセストークンまたはIDトークン)をAVPのIsAuthorizedWithToken APIに渡します。
  3. AVPがCognito User PoolのJWKSでトークンを自動検証し、claimsをEntityとして解析します。
  4. Cedarポリシーに照らしてAllow or Denyを返します。
# Identity Source 登録(CLIの場合)
aws verifiedpermissions create-identity-source \
  --policy-store-id <policy-store-id> \
  --configuration '{
 "cognitoUserPoolConfiguration": {
"userPoolArn": "arn:aws:cognito-idp:ap-northeast-1:<account-id>:userpool/<pool-id>",
"clientIds": ["<app-client-id>"]
 }
  }' \
  --principal-entity-type "User" \
  --region ap-northeast-1

Cognitoカスタム属性のCedarへの伝達

Cognitoのカスタム属性(custom:tenantIdcustom:role等)はJWTに含まれます。IsAuthorizedWithToken処理時にAVPがclaimsをコンテキストとして自動解析するため、CedarスキーマのcontextフィールドにtenantIdを定義しておくと、ポリシー内でcontext.token.tenantIdとして参照できます。

参照: AWS「Cognito identity sources


5-5. 実装例: Lambda authorizer → AVP IsAuthorizedWithToken

API GatewayのLambda authorizerからAVPを呼び出すパターンはマイクロサービスアーキテクチャでの代表的な実装です。アプリコードに認可ロジックを分散させず、横断的に制御できます。

import boto3
import json

avp_client = boto3.client('verifiedpermissions', region_name='ap-northeast-1')
POLICY_STORE_ID = '<your-policy-store-id>'

def handler(event, context):
 token = event.get('authorizationToken', '').replace('Bearer ', '')

 # IsAuthorizedWithToken: CognitoのJWTをそのまま渡す
 response = avp_client.is_authorized_with_token(
  policyStoreId=POLICY_STORE_ID,
  identityToken=token,# IDトークン(またはaccessToken=で指定)
  action={
'actionType': 'Action',
'actionId': 'Document::read'
  },
  resource={
'entityType': 'Document',
'entityId': _extract_doc_id(event['methodArn'])
  }
 )

 decision = response['decision']  # 'ALLOW' or 'DENY'
 effect = 'Allow' if decision == 'ALLOW' else 'Deny'
 return _generate_policy('user', effect, event['methodArn'])

def _extract_doc_id(method_arn: str) -> str:
 # ARN末尾のパスコンポーネントをリソースIDとして利用
 return method_arn.split('/')[-1]

def _generate_policy(principal_id: str, effect: str, resource: str) -> dict:
 return {
  'principalId': principal_id,
  'policyDocument': {
'Version': '2012-10-17',
'Statement': [
 {
  'Action': 'execute-api:Invoke',
  'Effect': effect,
  'Resource': resource
 }
]
  }
 }

東京リージョン対応: Amazon Verified Permissionsはap-northeast-1(東京)でGA済みです(2023年6月GA)。

§5 設計まとめ: IAM + Cedar の2層認可

  • IAM ABAC(Vol1)はAWSリソース境界を守る。Cedar/AVP(本Vol)はアプリ機能境界を守る。両者を使い分けることで、インフラ〜アプリを通した多層防御が実現する。
  • CedarスキーマにtenantIdを必須フィールドとして定義し、全ポリシーのwhen条件で参照することでテナント越境を構造的に防止できる。
  • Cognito Identity Source登録 + IsAuthorizedWithToken APIでJWT → Cedar評価がシームレスに繋がる。
  • Policy Templateとオンボーディング自動化を組み合わせると、テナント追加のたびにポリシーを手書きする運用負荷をゼロにできる。

6. M2M認証 — client_credentials grant とSaaS API間連携

M2M 認証フロー (client_credentials)
図5: M2M 認証フロー — client_credentials grant

6-1. ★重要前提: Cognitoのtoken exchange非対応とclient_credentials grantの位置づけ

Cognitoの/oauth2/tokenエンドポイントが対応しているgrant typeは以下のとおりです。

grant_typeRFCCognito対応
authorization_codeRFC 6749 §4.1
client_credentialsRFC 6749 §4.4○(M2M認証)
refresh_tokenRFC 6749 §6
urn:ietf:params:oauth:grant-type:token-exchangeRFC 8693✗ ネイティブ非対応

token exchange(RFC 8693)とは: あるトークンを別のトークンに交換するパターン(例: サービスAのアクセストークン → サービスBの委任トークン)。マイクロサービス間でIDを伝播する際に使われます。Cognitoはこれをネイティブにサポートしていないためアーキテクチャ設計の前提として必ず確認してください。同様の用途が必要な場合は、カスタムLambdaでクレームを変換・再署名するパターンか、専用IdP(Keycloak等)を前段に置くアーキテクチャを採用します。

SaaS API間の通常のサービス認証(バックエンド → バックエンド)にはclient_credentials grantで十分です。

Resource Server とスコープ設計

Cognitoでは「Resource Server」にAPIのidentifierとカスタムスコープを登録し、App Clientに付与します。

# Resource Server 登録
aws cognito-idp create-resource-server \
  --user-pool-id <pool-id> \
  --identifier "https://api.example.com" \
  --name "SaaS Backend API" \
  --scopes '[
 {"ScopeName":"read","ScopeDescription":"Read access"},
 {"ScopeName":"write","ScopeDescription":"Write access"}
  ]' \
  --region ap-northeast-1

# App Client 作成(client secretあり、client_credentialsのみ許可)
aws cognito-idp create-user-pool-client \
  --user-pool-id <pool-id> \
  --client-name "service-account-backend" \
  --generate-secret \
  --allowed-o-auth-flows "client_credentials" \
  --allowed-o-auth-scopes "https://api.example.com/read" \
  --allowed-o-auth-flows-user-pool-client \
  --region ap-northeast-1

参照: AWS「Cognito token endpoint」・RFC 6749 §4.4・RFC 8693

6-2. client_credentials grant の基本フロー

トークン取得リクエスト

# Basic認証でClient ID + Secretを送信
curl -X POST \
  "https://<cognito-domain>.auth.ap-northeast-1.amazoncognito.com/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "<client-id>:<client-secret>" \
  -d "grant_type=client_credentials&scope=https://api.example.com/read"

レスポンス例:

{
  "access_token": "eyJraWQiOiJrZXlpZCIsImFsZyI6IlJTMjU2In0...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

注意点: client_credentialsで発行されたアクセストークンはid_tokenを含まず、refresh_tokenも発行されません(グラント仕様通り)。期限切れ後は再度トークン取得が必要です。

トークンキャッシュの実装(必須)

import boto3
import time
import base64
import urllib.request
import urllib.parse
import json

class CognitoM2MClient:
 def __init__(self, domain: str, client_id: str, client_secret: str, scope: str):
  self._domain = domain
  self._client_id = client_id
  self._client_secret = client_secret
  self._scope = scope
  self._token = None
  self._expires_at = 0  # unix timestamp

 def get_access_token(self) -> str:
  # expires_in - 60秒以内は再利用(早めに更新して切れ目をなくす)
  if self._token and time.time() < self._expires_at - 60:
return self._token
  self._token, expires_in = self._fetch_token()
  self._expires_at = time.time() + expires_in
  return self._token

 def _fetch_token(self):
  url = f"https://{self._domain}/oauth2/token"
  credentials = base64.b64encode(
f"{self._client_id}:{self._client_secret}".encode()
  ).decode()
  data = urllib.parse.urlencode({
'grant_type': 'client_credentials',
'scope': self._scope
  }).encode()
  req = urllib.request.Request(
url, data=data,
headers={
 'Authorization': f'Basic {credentials}',
 'Content-Type': 'application/x-www-form-urlencoded'
}
  )
  with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
  return result['access_token'], result['expires_in']

6-3. 料金の最新動向(2024-2025年)

Cognitoは2024年5月(2024年5月9日)にM2Mのティア課金(per-app-client + per-token-request)を導入し、さらに2025年11月にper-app-client(アプリクライアント単位の月額固定費)の課金次元を撤廃しました。

現行課金モデル(2025年時点)

項目内容
MAU無料枠50,000 MAU/月まで無料(M2M Userはこれとは別カウント)
M2M月間リクエスト無料枠3,000 M2Mリクエスト/月まで無料
超過分段階単価(Cognitoの料金ページで確認)
per-app-client月額固定費廃止済み(旧モデルにあったApp Client単位の固定費は撤廃)
⚠️ M2M課金の落とし穴

  • トークンキャッシュ未実装は致命的: client_credentialsのデフォルト有効期限は1時間。リクエストのたびにトークン取得するとM2Mリクエスト数が爆増し無料枠を超過する。必ずメモリキャッシュを実装すること(上記6-2の実装例参照)。
  • アクセストークン有効期限の調整: App Client設定でアクセストークン有効期限を最大1日(86400秒)まで延長可能。サービス間通信の安定性と更新頻度のバランスで設定する。

参照: AWS「Amazon Cognito の料金

6-4. SaaS API間連携の実装 — スコープ設計とテナント分離

テナント別スコープ設計

マルチテナントSaaSでM2M認証にテナント分離を加える方法は主に2つあります。

方式設計例特徴
テナント別App ClientテナントごとにApp Clientを発行完全分離・シークレット管理が複雑。テナント数が固定・少数の場合に向く。
共有App Client + カスタムスコープhttps://api.example.com/tenant-acme:read のようにスコープ名にテナントIDを含めるシンプル・テナント数が多い/動的な場合に向く。スコープ数爆発に注意。

中小テナント数(〜100件程度)はテナント別App Clientが監査上の明瞭さから推奨です。テナント動的生成・自動オンボーディングが必要な場合はカスタムスコープパターンを採用します。

受信側でのトークン検証(スコープ確認まで必須)

import urllib.request
import json
import jwt# PyJWT

COGNITO_POOL_ID = '<pool-id>'
COGNITO_REGION = 'ap-northeast-1'
JWKS_URL = (
 f"https://cognito-idp.{COGNITO_REGION}.amazonaws.com/"
 f"{COGNITO_POOL_ID}/.well-known/jwks.json"
)

def verify_m2m_token(token: str, required_scope: str) -> dict:
 jwks_client = jwt.PyJWKClient(JWKS_URL)
 signing_key = jwks_client.get_signing_key_from_jwt(token)

 payload = jwt.decode(
  token,
  signing_key.key,
  algorithms=["RS256"],
  # client_credentials tokenはaud claimを含まないことがある
  options={"verify_aud": False}
 )

 # スコープ検証(署名検証のみでは不十分)
 token_scopes = payload.get('scope', '').split(' ')
 if required_scope not in token_scopes:
  raise PermissionError(
f"Required scope '{required_scope}' not found in token. "
f"Token scopes: {token_scopes}"
  )

 return payload

注意: client_credentialsで発行されたアクセストークンはCognitoの実装上audクレームを含まないことがあります。verify_aud=Falseとして検証するか、audをResource Server identifierで明示的に確認してください。

最小権限の原則

  • App Clientに付与するスコープは実際に使用するものだけに絞る。
  • writeスコープは読み取り専用のバッチ処理には付与しない。
  • サービスアカウントごとにApp Clientを分け、インシデント発生時は個別無効化できる体制にする。
  • 定期的に(四半期ごと等)未使用App Clientの棚卸しを実施する。
§6 設計まとめ: M2M認証のポイント

  • CognitoはRFC 6749 client_credentials を実装。RFC 8693 token exchangeはネイティブ非対応 — この前提でサービス間認証アーキテクチャを設計すること。
  • トークンキャッシュ必須。リクエストごとに取得するとM2M課金が急増する(expires_in – 60秒以内は再利用)。
  • テナント分離にはApp Client単位またはカスタムスコープ名にテナントIDを含める方式を、テナント数・動的生成要件に合わせて選択する。
  • 受信側は署名検証に加えてスコープ検証を必ず実装する。署名だけでは権限範囲を確認できない。

7. トークン戦略と検証 — JWT・ローテーション・per-tenant失効

JWT トークン戦略・per-tenant 失効設計
図6: JWT トークン戦略と per-tenant 失効設計

7-1. JWT検証パイプライン

Cognitoが発行するトークンは3種類あります。アクセストークン(APIアクセス権限)、IDトークン(ユーザー属性)、リフレッシュトークン(トークン更新用)です。アクセストークンとIDトークンはどちらもRS256署名付きJWTで、検証には署名・クレーム・テナント属性の確認が必要です。

署名検証と公開鍵取得

CognitoはUser Pool単位でJWKSエンドポイントを公開しています。サービス起動時にこのエンドポイントから公開鍵をキャッシュし、以降の検証に使います。

https://cognito-idp.<region>.amazonaws.com/<userPoolId>/.well-known/jwks.json

必須検証項目

項目確認内容
kid ヘッダーJWKSから対応する公開鍵を選択
RS256署名公開鍵で署名を検証
isshttps://cognito-idp.<region>.amazonaws.com/<userPoolId> と一致
aud(IDトークン)アプリのクライアントIDと一致
client_id(アクセストークン)アプリのクライアントIDと一致
exp有効期限が現在時刻より未来
token_useid または access(用途に合致しているか)

API Gateway での検証統合

HTTP API では Cognito User Pool を Authorizer として直接アタッチでき、署名検証・aud/iss 確認が自動化されます。REST API の場合は Lambda Authorizer でカスタムロジックを追加します。

import json
import urllib.request
from jose import jwt

REGION = "ap-northeast-1"
USER_POOL_ID = "ap-northeast-1_xxxxxxxxx"
APP_CLIENT_ID = "xxxxxxxxxxxxxxxxxxxxxxxx"

def handler(event, context):
 token = event.get("authorizationToken", "").replace("Bearer ", "")

 # JWKS 取得(本番環境では TTL 付きキャッシュを使うこと)
 jwks_url = (
  f"https://cognito-idp.{REGION}.amazonaws.com"
  f"/{USER_POOL_ID}/.well-known/jwks.json"
 )
 with urllib.request.urlopen(jwks_url) as res:
  jwks = json.loads(res.read())

 claims = jwt.decode(
  token, jwks,
  algorithms=["RS256"],
  audience=APP_CLIENT_ID,
  issuer=f"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}"
 )

 tenant_id = claims.get("custom:tenant_id")
 if not tenant_id:
  raise Exception("Unauthorized")

 return generate_policy(
  claims["sub"], "Allow", event["methodArn"],
  context={"tenantId": tenant_id}
 )

Lambda Authorizer の戻り値に context としてテナントIDを含めると、バックエンド Lambda は $context.authorizer.tenantId から取得できます。これにより、各マイクロサービスはトークン再検証なしにテナント情報を利用できます。

テナント情報の JWTクレーム埋め込み戦略

Cognito では Pre Token Generation Lambda トリガー(v2)を使ってカスタムクレームをアクセストークンとIDトークンに追加できます。フェデレーション経由のユーザーであっても、SAML属性マッピングで設定したカスタム属性をそのままクレームに反映できます。

def handler(event, context):
 attrs = event["request"]["userAttributes"]

 event["response"]["claimsAndScopeOverrideDetails"] = {
  "idTokenGeneration": {
"claimsToAddOrOverride": {
 "tenant_id":attrs.get("custom:tenant_id", ""),
 "tenant_tier": attrs.get("custom:tenant_tier", "standard"),
}
  },
  "accessTokenGeneration": {
"claimsToAddOrOverride": {
 "tenant_id": attrs.get("custom:tenant_id", ""),
}
  }
 }
 return event

アクセストークンへのカスタムクレーム追加は Pre Token Generation v2 トリガー(TOKEN_V2_GENERATION)でのみ利用可能です。v1 トリガーはIDトークンのみ対応しているため、User Pool の設定確認が必要です。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html

7-2. refresh tokenローテーションと失効

Refresh Token の有効期限設定

Cognito のリフレッシュトークンは User Pool の App Client ごとに有効期限を設定します(範囲: 1時間〜10年、デフォルト: 30日)。エンタープライズ顧客向けに短い有効期限を設定すると、定期的な再認証を強制できます。

Token Revocation の有効化

User Pool の App Client 設定で Token Revocation を有効にすると、RevokeToken API でリフレッシュトークンを即時無効化できます。無効化後、そのリフレッシュトークンで発行したアクセストークンも期限切れまで利用できなくなります(CognitoはATのブロックリストを内部管理します)。

Global Sign-Out

ユーザーが全デバイスからのサインアウトを要求した場合、または管理者が強制サインアウトを実行する場合:

import boto3

cognito = boto3.client("cognito-idp", region_name="ap-northeast-1")

# ユーザー自身による全デバイスサインアウト(現在のアクセストークンが必要)
cognito.global_sign_out(AccessToken=current_access_token)

# 管理者による強制サインアウト(アクセストークン不要、管理者権限のみ)
cognito.admin_user_global_sign_out(
 UserPoolId=USER_POOL_ID,
 Username=username
)

per-tenant トークン失効 — テナント契約終了時の3パターン

テナントが解約または一時停止した際、そのテナントの全ユーザーのセッションを即時無効化する必要があります。

(1) Admin Global Sign-Out 一括実行(シンプル・即効性あり)

def revoke_tenant_sessions(tenant_id: str, user_pool_id: str):
 cognito = boto3.client("cognito-idp", region_name="ap-northeast-1")
 paginator = cognito.get_paginator("list_users")

 for page in paginator.paginate(
  UserPoolId=user_pool_id,
  Filter=f'custom:tenant_id = "{tenant_id}"'
 ):
  for user in page["Users"]:
try:
 cognito.admin_user_global_sign_out(
  UserPoolId=user_pool_id,
  Username=user["Username"]
 )
except cognito.exceptions.UserNotFoundException:
 pass

テナントユーザーが数百人規模でも、ページネーターで全件処理できます。ただしリフレッシュトークンの再利用まではカバーしきれないため、後述のブロック属性パターンと組み合わせます。

(2) テナントステータス属性 + Lambda Authorizer ブロック

カスタム属性 custom:tenant_statussuspended に更新し、Lambda Authorizer でこの値をチェックします。トークンが有効期限内であってもアクセスを拒否できます。Cognito トークン取得自体を防ぐわけではありませんが、全APIエンドポイントを守れます。

# Lambda Authorizer 内の追加チェック
user = cognito.admin_get_user(
 UserPoolId=USER_POOL_ID,
 Username=claims["cognito:username"]
)
attrs = {a["Name"]: a["Value"] for a in user["UserAttributes"]}
if attrs.get("custom:tenant_status") == "suspended":
 raise Exception("Unauthorized: tenant suspended")

(3) JTI DenyList(高速遮断・CloudFront + DynamoDB)

アクセストークンの jti クレームをDynamoDBに登録し、Lambda@Edge で参照する方式です。トークン有効期限(最大60分)が切れるまでの短期間ブロックに適しています。実装コストは高めですが、即時遮断と監査ログを同時に実現できます。

設計推奨: エンタープライズ向けSaaSでは (1) + (2) の組み合わせを基本とし、セキュリティ要件が高いテナント向けに (3) を追加するのが現実的です。

source_url: https://docs.aws.amazon.com/cognito/latest/developerguide/token-revocation.html


8. まとめ・次のステップ・関連記事

8-1. 本記事のまとめ

Vol2で扱ったエンタープライズ認証・認可の実装要点を整理します。

  • B2Bフェデレーション(§3): Cognito User Pool にテナント別 SAML 2.0 / OIDC IdP を接続。SP-initiated フロー・属性マッピング・ドメインベースIdPルーティングにより、顧客企業の既存IdP(Okta・Entra ID・OneLogin等)とシームレスに統合できます。
  • SCIMの現実解(§4): Cognito は SCIM 2.0 をネイティブに備えません。エンタープライズ要件では JITプロビジョニング・カスタム SCIM エンドポイント(API Gateway + Lambda)・IAM Identity Center 前段の3択から要件に応じて選定します。
  • Amazon Verified Permissions / Cedar(§5): Vol1 で構築したインフラ層ABAC に加え、アプリ層のきめ細かい認可を Cedar ポリシーで表現します。テナント境界はポリシーテンプレートとエンティティグループで分離し、IsAuthorized API で判定します。
  • M2M認証(§6): Cognito の client_credentials grant(RFC 6749)でサービス間認証を実現します。resource server と scope で最小権限を設計し、2025年以降の料金体系(MAUベース・M2M専用ティア廃止)を前提にコスト見積もりを行います。
  • トークン戦略(§7): JWT検証は署名・issaudexp・テナントクレームを必ず確認します。Pre Token Generation v2 トリガーでアクセストークンにテナントIDを埋め込み、Lambda Authorizer を経由することで下流マイクロサービスへの伝播を簡素化できます。per-tenant 失効は Admin Global Sign-Out + ブロック属性の組み合わせが現実解です。

設計判断サマリ

トピック採用パターン主な理由
フェデレーション構成単一UserPool + テナント別外部IdPテナント数増加時のUserPoolコスト・管理負荷を抑制
SCIM対応JIT(スモールスタート)→ カスタムSCIMエンドポイント(大規模)Cognitoネイティブ非対応のため現実解を選定
アプリ層認可Amazon Verified Permissions(Cedar)IAM ABACと役割分担・Cedar言語で複雑な認可ルールを宣言的に記述
M2M認証Cognito client_credentials + resource server/scopeRFC 6749準拠・マネージドかつCognito統合で運用コスト最小
JWT検証Lambda Authorizer(REST API)/ Cognito Authorizer(HTTP API)テナントクレーム検証の有無でどちらを選択するか判断
per-tenant失効Admin Global Sign-Out + ブロック属性即時遮断と実装コストのバランスが最適

Vol1 → Vol2 設計の接続点

Vol1で設定したテナント識別子(タグ・カスタム属性・スコープ)が、Vol2のすべてのレイヤーで前提となっています。フェデレーション後のJWT・Cedarのエンティティ・M2Mスコープすべてに tenant_id が流れる設計を意識してください。Vol1で識別子設計を省略していると、Vol2の各コンポーネントを後付けで統合する際に大きなリファクタが発生します。

実装でよくある詰まりポイントまとめ

Vol2の各トピックで繰り返し発生する詰まりポイントをまとめます。

詰まりポイント原因対策
フェデレーション後に custom:tenant_id がJWTに入らないSAML属性マッピング設定漏れ または Pre Token Generation v1 トリガー(アクセストークンに追加できない)SAMLマッピングを再確認、Pre Token Generation v2 に移行
テナントAのIdPでテナントBにログインできてしまうテナント越境チェックが Lambda Authorizer に未実装custom:tenant_id クレームと要求リソースのテナント所有者を照合するロジックを必ず追加
CognitoにSCIMエンドポイントを設定しようとしてもないCognitoはSCIM 2.0非対応(IAM Identity Center が対応)§4の3パターンを再確認、要件に応じてJIT/カスタムSCIM/Identity Centerを選択
IsAuthorized で常にDENYが返るCedarスキーマのエンティティ定義不備(principal/resource型名のミス)Cedar Playgroundでスキーマとポリシーを単体検証してからVerified Permissionsに適用
M2Mトークン取得でClient Secret認証エラーBasic認証ヘッダーのエンコード形式ミス(base64(client_id:client_secret)Cognitoの/oauth2/tokenはHTTP Basic認証形式を要求、URLエンコード後にBase64
per-tenant失効後もAPIが通ってしまうAdmin Global Sign-OutはRefresh Tokenのみ対象。アクセストークンは有効期限まで有効Lambda Authorizerで custom:tenant_status = suspended チェックを追加(§7-2 パターン2)
アクセストークンの検証で invalid_token エラーaud ではなく client_id クレームを使うべき(アクセストークン固有)IDトークンは aud、アクセストークンは client_id クレームを検証

8-2. 次のステップ

Vol3(予定): 本番運用監視・監査・コスト最適化

Vol2でエンタープライズ認証・認可の基盤が整ったら、Vol3では本番運用を支える可視化とコスト管理を扱います。

監視・監査の強化

対象ツール目的
per-tenant 認証ログCloudTrail + CloudWatch Logs Insights失敗ログイン急増・想定外リージョンのアラート
Verified Permissions 認可CloudWatch Metrics(DENY率)権限設計ギャップの継続改善
M2M トークン発行CloudTrail InitiateAuth ログスコープ逸脱・大量発行の検知
Cognito ユーザー活動CloudWatch Contributor Insightsテナント別MAU傾向とコスト予測

コスト最適化

Cognito の MAU ベース課金(2024年改定)では、非アクティブユーザーが蓄積するとコストが膨らみます。定期的な休眠ユーザーの無効化と App Client の棚卸しが重要です。Verified Permissions はポリシーストアのリクエスト数課金です。Cedar ポリシーの肥大化を防ぐため、テナントオフボーディング時にエンティティを削除するワークフローを組み込んでください。

テナントオフボーディング自動化

解約フローを Step Functions で自動化することで、手動対応のリスクを排除できます。推奨フロー:

  1. 全セッション失効(admin_user_global_sign_out 一括実行)
  2. ユーザー属性更新(custom:tenant_status = disabled
  3. Cognito App Client 無効化
  4. Cedar policy store からテナントエンティティ削除
  5. データアーカイブ(S3 Glacier 移行)
  6. 監査証跡の長期保存(CloudTrail → S3 → Glacier)

実装前チェックリスト

Vol2 の実装を開始する前に、以下を確認してください。

  • [ ] Vol1 でテナント識別子(custom:tenant_id)がCognito User Pool カスタム属性として設定済み
  • [ ] SAMLテスト用IdP(Okta Developer / Azure AD 評価テナント / Keycloak)が準備済み
  • [ ] Cognito User Pool の Essentials ティア(passkey 使用時)が有効化済み
  • [ ] Amazon Verified Permissions の policy store が作成済み(リージョン: ap-northeast-1)
  • [ ] Lambda Authorizer 用の IAM ロールに cognito-idp:AdminGetUser 権限が付与済み

Vol2 実装完了確認チェックリスト

実装後の動作確認に使用してください。

  • [ ] テナントAのSAML IdPアカウントでログインし、Cognitoトークンに custom:tenant_id = tenant-a が含まれる
  • [ ] テナントAのIdPアカウントでテナントBのリソースにアクセスしようとすると 403 が返る(テナント越境防止)
  • [ ] Cedar IsAuthorized API で admin ロールは DELETE が ALLOW、viewer ロールは DENY と返る
  • [ ] client_credentials grant で取得したM2Mトークンが API Gateway で受け入れられる
  • [ ] テナント契約終了時に admin_user_global_sign_out 実行後、リフレッシュトークンが使えなくなる
  • [ ] Lambda Authorizer で custom:tenant_status = suspended のユーザーが 401 で拒否される

読者アクション CTA

実装を進める際は、以下の公式リソースを一次ソースとして参照してください。

8-3. 関連記事クロスリンク

本シリーズは SaaS/マルチテナントアーキテクチャを「基盤構築(Vol1)」→「エンタープライズ認証(Vol2)」→「本番運用(Vol3)」の3段階で体系的にカバーします。

Vol1 → Vol2 → Vol3 シリーズ構成

Vol1 でテナント分離基盤(Silo/Pool/Bridge)・セルベース設計・従量計測・オンボーディング自動化を確立したうえで、Vol2(本記事)でエンタープライズ顧客の認証要件(B2B SSO・SCIM・Verified Permissions・M2M・トークン管理)を追加します。Vol3では本番稼働後の監視・監査・コスト最適化・テナントオフボーディング自動化を扱う予定です。各Volは独立して読めますが、Vol1 → Vol2 → Vol3 の順で通読すると SaaS アーキテクチャの全体像が最もクリアになります。

📌 SaaSマルチテナント本番運用シリーズ

  • Vol1: テナント分離(Silo/Pool/Bridge)・セルベース・従量計測・オンボーディング
  • Vol2(本記事): エンタープライズ認証連携(B2B SSO・SCIM・Verified Permissions・M2M・トークン戦略)
  • Vol3(予定): 本番運用監視・監査・コスト最適化・テナントオフボーディング自動化

▶ Vol1: テナント分離・セルベース・従量計測・オンボーディング編を読む