xyz-log.
トップへ戻る
Tech / 2026.03.07 New / 19 分 ...

Azure AD × CognitoによるSAML認証:SPAからiOSへの手渡しと完全なるSLOの探求

ZOLXA

ZOLXA

Mastermind

Azure AD × CognitoによるSAML認証:SPAからiOSへの手渡しと完全なるSLOの探求

Sponsored

Azure AD × CognitoによるSAML認証:SPAからiOSへの手渡しと完全なるSLOの探求

エンタープライズ領域におけるモバイルアプリケーション開発において、「社内アカウントでログインさせたい」という要件は避けて通れない。 その際、Azure Active Directory (Azure AD) を Identity Provider (IdP) とし、AWS Cognito を Service Provider (SP) 兼 認証ブローカーとして間に挟む構成は非常に実用的だ。

しかし、単純に Cognito の Hosted UI(マネージド型のログイン画面)を iOS アプリ内で開くだけなら話は簡単だが、実務では「ブランド統一されたWebのポータル画面(SPA)を経由させたい」あるいは「Web側のセッションとシームレスに連動させたい」といった複雑な要件が降ってくる。

本記事では、「Azure AD と Cognito を SAML で連携し、SPA(Web)で認証を開始。そこから Custom URL Scheme を用いて iOS アプリへ『認可コード(Authorization Code)』を引き渡し、ネイティブ側でトークンをセキュアに取得する」 というアーキテクチャの急所を解説する。 さらに、分散認証システムにおいて最も実装難易度が高く、バグの温床となりやすい 「Single Sign-Out (SLO)」 の確実な実現手法についても深く切り込んでいく。

インフラのポチポチ設定については公式ドキュメントに譲る。我々がフォーカスするのは、システム間の状態遷移、プロトコルの制約、そしてセキュリティ境界の設計だ。


1. 全体アーキテクチャと情報の流れ(Auth Code Flow with PKCE)

まず、この複雑な要件を成立させるための全体像を俯瞰する。 登場人物は以下の5名だ。

  1. Azure AD (IdP: 従業員のIDを管理し、実際のパスワード認証やMFAを行う)
  2. AWS Cognito User Pool (SP 兼 OIDC Provider: Azure AD と SAML で会話し、アプリ側には OAuth2.0 / OIDC の皮を被って接する)
  3. SPA (Web Portal) (Cognito の Authorization Endpoint をキックする起点)
  4. iOS App (Native) (最終的にアクセストークンを保持し、APIを叩く主体)
  5. Backend API (Cognito が発行した JWT トークンを検証する)

1.1. 致命的なアンチパターン:トークンの直接渡し

ここで絶対にやってはいけない設計がある。 それは「SPA側で Cognito から Access Token / ID Token まで取得してしまい、そのトークン本体を Custom URL Scheme(例: myapp://auth?token=ey...)で iOS に渡す」というアプローチだ。

Custom URL Scheme (ディープリンク) のペイロードは、OSレベルのルーティングを通過するため、極端な話、全く同じ Custom URL Scheme を宣言した悪意のある別のアプリがデバイスにインストールされていた場合、トークンが横取りされる(URL Hijacking)リスクが払拭できない。

1.2. 正攻法のアプローチ:PKCEを伴う認可コードの受け渡し

したがって、正解は 「認可コード(Authorization Code)」 のみを受け渡すことだ。 認可コード自体は、単体では何の意味も持たず、トークンと交換するためには**「事前に発行したクリプトグラフィックな秘密(PKCE: Proof Key for Code Exchange)」**が必要になるからだ。

これを成立させるためのシーケンスは以下のようになる。

Loading diagram...

2. アーキテクチャの急所:SPAとiOSの連携境界

上述のシーケンスにおいて、シニアエンジニアが最も警戒すべきは「状態の管理」と「プラットフォーム間の境界」である。

2.1. PKCEの生成主体は「iOS」でなければならない

なぜPKCEの code_verifiercode_challenge を iOS 側で生成し、SPA にわざわざクエリパラメータで渡すのか。 「SPA側で生成して、認可コードと一緒に iOS に渡せばいいのでは?」と考えたなら、それはセキュリティインシデントの引き金となる。

もしSPA側で code_verifier を生成し、それを myapp://callback?code=XXX&verifier=YYY とURLスキームで渡してしまうと、前述の URL Hijacking が起きた際に、悪意のあるアプリが認可コードと Verifier の両方を手に入れてしまい、トークンを不正に取得できてしまう。

「検証するための秘密(Verifier)は、通信経路(URL Scheme)に絶対に乗せず、トークン交換を行う主体(iOSアプリ内のセキュアなメモリ)が最初から最後まで保持し続ける」 これがPKCE導入の絶対のルールである。

2.2. ASWebAuthenticationSession の挙動と SFSafariViewController の罠

iOSアプリからSPAを開く際、SFSafariViewController や標準ブラウザへの遷移(UIApplication.shared.open)を使おうとするケースがあるが、これは推奨されない。最適解は ASWebAuthenticationSession である。

swift
// iOS側の実装イメージ import AuthenticationServices class AuthService: NSObject { var authSession: ASWebAuthenticationSession? func startAuth(pkceChallenge: String) { // SPAのURLを構築。PKCEのチャレンジ値を渡す let urlString = "https://your-spa-portal.com/login?challenge=\(pkceChallenge)&method=S256" guard let url = URL(string: urlString) else { return } // myapp:// へのリダイレクトを監視する let scheme = "myapp" authSession = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { callbackURL, error in guard error == nil, let callbackURL = callbackURL else { // キャンセルやエラーのハンドリング return } // URLから認可コードを抽出 let queryItems = URLComponents(string: callbackURL.absoluteString)?.queryItems if let code = queryItems?.first(where: { $0.name == "code" })?.value { // この code と、手元に秘匿している code_verifier を使って // バックグラウンドで Cognito の /oauth2/token を叩く self.exchangeToken(code: code) } } // セッションやクッキーを引き継ぐ設定 (iOS 13+) authSession?.presentationContextProvider = self authSession?.start() } }

ASWebAuthenticationSession は、OSレベルで「これは認証ブロックである」というアラート(「"App"がサインインのために"your-spa-portal.com"を使用しようとしています」)を出し、システムブラウザのクッキーやセッション情報を安全に共有する。 これにより、「実は直前にSafariでAzure ADにログインしていた」という場合、ユーザーに再度パスワードを打たせることなく、シームレスにSSOを完結させることができる。

2.3. SPA側の「中継器」としての徹し方

Web SPA側の役割は、極端な話「ただの土管」である。 ユーザーがSPAにアクセスした際、URLに challenge が含まれていれば、SPAは自身では認証フローを完了させず、Cognitoの /oauth2/authorize エンドポイントへそのままバケツリレーをする。

javascript
// SPA (React/Vue/Vanilla等) のログイン画面初期化処理 const urlParams = new URLSearchParams(window.location.search); const challenge = urlParams.get("challenge"); const method = urlParams.get("method") || "S256"; if (challenge) { // iOS等から呼び出されたNativeログインフロー // CognitoのAuthorizeエンドポイントを構築 const cognitoDomain = "https://your-user-pool.auth.ap-northeast-1.amazoncognito.com"; const clientId = "YOUR_APP_CLIENT_ID"; // SPA自身をコールバックに指定。Cognito上でもこのURLを許可しておくこと。 const redirectUri = encodeURIComponent( "https://your-spa-portal.com/callback", ); const authUrl = `${cognitoDomain}/oauth2/authorize?client_id=${clientId}&response_type=code&scope=openid+profile&redirect_uri=${redirectUri}&code_challenge=${challenge}&code_challenge_method=${method}`; // 即座にCognito (及びそこからAzure AD) へリダイレクト window.location.href = authUrl; } else { // Web単独でログインする場合の処理(本筋から逸れるため割愛) }

そして、CognitoからSPAの /callback?code=XXXXX に戻ってきた際は、自身でトークン交換を行わず、iOSへと送り返す。

javascript
// SPA の /callback ルートの処理 const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); if (code && isNativeFlow()) { // セッションストレージ等でNativeフロー中かを判定 // Custom URL Scheme を叩いて iOS に制御を戻す // ※ ユーザーの操作なしに即座に window.location.href に代入する window.location.href = `myapp://callback?code=${code}`; }

これで、強固なセキュリティを担保したまま、SPA経由でのiOSアプリ認証が完了する。


3. 深淵の課題:Single Sign-Out (SLO) の完全征服

ログイン(SSO)が成功したなら、我々は**ログアウト(SLO: Single Sign-Out)**に対峙しなければならない。 分散システムにおけるログアウトとは、「あるアプリのログアウトボタンを押したら、紐づくすべてのセッション(IdPのセッション、他のSPのセッション)も連動して破棄される」ことだ。

SSOは「みんなで一つの鍵束を使う」幸福な体験だが、SLOは「ある日突然、合鍵をすべて無効化しなければならない」という運用上の悪夢である。 特に SAML と OIDC (Cognito) が混在するアーキテクチャでは、設定の1ミスで「アプリからログアウトしたけど、Azure ADのセッションが残っているため、次回ログインを押すとパスワードなしで勝手に入れてしまう(幽霊セッション問題)」が頻発する。

3.1. SAML連携におけるログアウトの難しさ

Azure AD と Cognito が SAML で繋がっている場合、SLOを成功させるには、以下のセッションをすべて正しい順序で破棄しなければならない。

  1. iOS アプリ内のローカルセッション(Keychain内のToken削除)
  2. Cognito のセッション (Cognito Hosted UI の Cookie 等)
  3. Azure AD のセッション (Azure AD 側の Cookie 等)

3.2. Cognito の /logout エンドポイントの仕様と限界

Cognito には /logout エンドポイントが用意されている。(※ 以前は非公式な振る舞いが多かったが、現在はドキュメント化されている)。 iOS アプリからユーザーが「ログアウト」ボタンを押した際、以下のようにブラウザ(今回なら ASWebAuthenticationSession)経由で Cognito の /logout をキックする。

http
GET https://<your-domain>.auth.<region>.amazoncognito.com/logout? client_id=<your-client-id>& logout_uri=<encode-myapp://logout-callback>

しかし、これだけでは Azure AD 側のセッションは消えない。

Cognito の /logout は、あくまで「Cognito がブラウザに焼いたセッション Cookie を消す」だけである。 IdP (Azure AD) までリダイレクトしてIdP側のログアウトを誘発する(SAML LogoutRequestを投げる)機能は、実は Cognito の標準動作として**完全には保証されていない(もしくはIdPの構成に強く依存する)**ケースが多い。

3.3. 確実な SLO を実現するアプローチ

この「幽霊セッション」を確実に殺すには、アーキテクチャ上の工夫が必要になる。 シニアエンジニアとして取るべき戦術は以下の2パターンのいずれかだ。

パターンA: Azure AD のログアウトエンドポイントを直接叩く (Force IdP Logout)

Cognito に期待せず、アプリケーション側で IdP (Azure AD) のログアウトエンドポイントを直接明示的に叩くアプローチだ。

  1. iOS アプリ側ローカルの Token を破棄
  2. ASWebAuthenticationSession で、Cognito ではなく Azure AD のエンドポイントを直接開く。 https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/logout?post_logout_redirect_uri=myapp://logout
  3. Azure AD 側のセッションが破棄され、コールバックでアプリに戻る。

メリット: Azure AD のセッションは確実に死ぬため、次回ログイン時は絶対に認証プロンプトが出る。 デメリット: Cognitoのシステムから見た場合、SAMLを通じた正式なログアウトプロトコル(IdP Initiated SLO)ではなくなる。ただし、モバイルアプリの場合、Cognito側で長寿命なセッションに依存することは少なく、Access/Refresh Tokenさえ適切にRevoke(後述)していれば実害は少ない。

パターンB: Token Revocation + ASWebAuthenticationSession の揮発性利用

実は近年、モバイルアプリで最も主流なアプローチは、「IdP側のセッションをあえて消そうと努力しない」ことである。 代わりに、ローカルのトークンを完全に無効化(Revoke)し、次のログイン時に ASWebAuthenticationSession の設定を「揮発性(Ephemeral)」として扱う。

Step 1: Token Revocation (無効化) ログアウト時、iOSアプリからバックグラウンド通信で Cognito の /oauth2/revoke エンドポイントに Refresh Token を投げ、トークンをサーバーサイドで無効化する。

http
POST https://<your-domain>.auth.<region>.amazoncognito.com/oauth2/revoke Content-Type: application/x-www-form-urlencoded client_id=<your-client-id>& token=<refresh-token>

これにより、漏洩したトークンが使われるリスクは無くなる。

Step 2: 次回ログイン時の Ephemeral Session の利用 (iOS 13+) これが肝である。iOS側で ASWebAuthenticationSession を立ち上げる際、prefersEphemeralWebBrowserSession プロパティを true に設定する。

swift
authSession = ASWebAuthenticationSession(...) { ... } // これを true にすると、Safariのクッキーや永続データを一切共有しない、 // プライベートブラウズのような真っ新な状態でウィンドウが開く。 authSession?.prefersEphemeralWebBrowserSession = true

パターンBのSLOフロー図(Token Revocation & Ephemeral Session)

このパターンのシーケンスは非常にシンプルかつ強固になる。

Loading diagram...

メリット: ブラウザが立ち上がるたびにクッキーが空であるため、Azure AD側にセッションがどれだけ残っていようが関係ない。ユーザーには必ず「ログイン画面(ID/パスワード入力またはMFA)」が表示される。IdP間の複雑なSLO連携を組む必要がなく、驚くほどアーキテクチャがシンプルになる。

デメリット: 真の意味での「シングルサインオン(他のアプリでログインした状態を使い回す)」の利便性は低下する。アプリAを開くたび、アプリBを開くたびにそれぞれ認証が走ることになる。

もし要件が「エンタープライズの厳格なセキュリティ」であり、「ログアウトしたなら次回は確実に認証させたい」のであれば、**パターンB(Token Revocation + Ephemeral Session)**を強く推奨する。 これこそが、SAML連携の泥沼( SLO対応でIdPとSPの間をタライ回しにされる現象)を回避する、現代のモバイルアーキテクチャにおけるエレガントな解である。


4. Swift実装におけるクラス設計指針

「Coordinator」の必要性

すべてを一つの ViewControllerViewModel に詰め込むのは愚の骨頂である。シニアエンジニアであれば、認証フローのような**「状態の変化がアプリ全体に影響を及ぼし、様々な画面遷移(ルーティング)を伴う副作用の塊」**を操作するためには、専用の指揮者たる「Coordinator(またはFacade)」が必要不可欠だと理解しているはずだ。

以下のように責務を分割し、抽象化されたインターフェース(Protocol)越しに会話させる設計を推奨する。

推奨される主要クラス群と責務

  1. AuthCoordinator
    • 全体の認証フローを指揮し、ルーティング(画面遷移)と副作用をコントロールする要(プロジェクトの規模によっては AuthManager と呼称されることもある)。
    • ログイン要求を受け、PKCEGenerator に暗号生成を依頼し、WebAuthService を起動する。返ってきた認可コードを TokenClient に渡してトークン化し、Keychainに保存する。成功の暁には、「未ログイン画面」から「ログイン済みホーム画面」へのWindowのRootViewControllerの差し替えや、グローバルなイベント通知(Combine / NotificationCenter)を発行する。この一連の副作用(Side Effect)をViewController内に書くと、Fat ViewController化と密結合の温床となるため、Coordinatorパターンが極めて有効に機能する。
  2. PKCEGenerator
    • 責務: 乱数生成(code_verifier)と、そのSHA-256ハッシュ+Base64URLエンコード(code_challenge)の導出。
    • ここは純粋なドメインロジック(暗号化ユーティリティ)として切り出し、状態を持たない Struct/Enum として定義するのが美しい。
  3. WebAuthService
    • 責務: ASWebAuthenticationSession のカプセル化。
    • 渡されたURL(SPAのログイン画面)を開き、指定された Custom URL Scheme(例: myapp://)で戻ってきたURLから code を抽出してコールバックで返す。UIのコンテキスト(ASWebAuthenticationPresentationContextProviding)もここで吸収する。
  4. TokenClient (API Client)
    • 責務: Cognito の /oauth2/token/oauth2/revoke と直接HTTP通信を行うレイヤー。
    • 受け取った認可コードやリフレッシュトークンを元にネットワークリクエストを行い、生レスポンスを TokenResponse モデルにデコードする。
  5. KeychainStorage
    • 責務: 取得した Access Token / ID Token / Refresh Token の安全な永続化。
    • メモリ上にキャッシュしつつ、必要に応じて iOS の Keychain Services (kSecClassGenericPassword等) を読み書きする。決して UserDefaults を使ってはいけない。

これらの依存関係をProtocolベースで注入(DI)可能にしておくことで、例えば「ネットワーク通信なしでトークン交換が成功したとみなすモック環境(UIテスト用)」などを容易に構築できる。


ここまで Custom URL Scheme (myapp://callback) を前提に解説してきたが、実はiOS 9以降、Apple は Universal Links (https://your-domain.com/auth/callback) という、より安全なディープリンク機構を提供している。

Custom URL Scheme の弱点

Custom URL Scheme は「早い者勝ち」だ。myapp:// というスキームを2つのアプリが同時に宣言した場合、どちらにルーティングされるかはOSの裁量に委ねられ、開発者は制御できない。PKCEがあるため認可コードだけでは攻撃者はトークンを取得できないが、「ユーザーが意図しないアプリに遷移してしまう」というUX上の問題は残る。

Universal Links は、ドメイン所有者が /.well-known/apple-app-site-association (AASA) ファイルをサーバーに配置し、Appleがそのドメインとアプリの紐付けを暗号学的に検証する。したがって、第三者のアプリがあなたのドメインのリンクを横取りすることは原理的に不可能だ。

json
// https://your-spa-portal.com/.well-known/apple-app-site-association { "applinks": { "apps": [], "details": [ { "appID": "TEAM_ID.com.yourcompany.yourapp", "paths": ["/auth/callback"] } ] } }

では、なぜ Custom URL Scheme がまだ使われるのか?

ASWebAuthenticationSessioncallbackURLScheme パラメータは、その名の通り URL Scheme しか受け付けない。Universal Links で直接コールバックを受けることができないのだ。これが Custom URL Scheme がモバイル認証フローで依然として主流である最大の理由である。

ただし、SPA側のコールバックURL自体を Universal Links 対応のURLにしておき(例: https://your-spa-portal.com/auth/callback?code=XXX)、SPA側で window.location.href = "myapp://..." にリダイレクトする代わりに、Universal Links で直接iOSアプリを開くハイブリッドアプローチも検討の余地がある。この場合、SPAの /auth/callback ページが Universal Links として処理され、アプリが直接起動する。


6. state パラメータによるCSRF対策

PKCEは「認可コードの横取り」を防ぐが、**CSRF(Cross-Site Request Forgery)**は防げない。 攻撃者が自身のアカウントで取得した認可コードを、被害者のブラウザにすり替えて注入する(=被害者を攻撃者のアカウントでログインさせる)攻撃が理論上可能だ。

OAuth2.0 の state パラメータはこれを防ぐ。

実装の要点

iOSアプリ側で認証フローを開始する際、暗号学的にランダムな state 値を生成し、ローカルに保持しておく。

swift
// AuthCoordinator 内 let state = UUID().uuidString // または SecRandomCopyBytes で生成 // state をメモリに保持(Keychainまでは不要、フロー中のみ有効) self.pendingState = state // SPA に渡すURLに state を含める let url = "https://your-spa-portal.com/login?challenge=\(challenge)&method=S256&state=\(state)"

SPA側はこの state を Cognito の /oauth2/authorize にそのまま転送し、Cognito はコールバック時にそのまま返してくる。iOSアプリがコールバックを受けた際、返ってきた state と手元に保持していた state が一致するかを必ず検証する。

swift
// コールバック受信時 guard let returnedState = queryItems?.first(where: { $0.name == "state" })?.value, returnedState == self.pendingState else { // state が一致しない → CSRF攻撃の可能性、フローを中断 return }

PKCEと state補完的な関係であり、両方を実装して初めてOAuth2.0の認可コードフローは完全にセキュアになる。


7. Access Token のリフレッシュ戦略

ログインが成功した後、永遠にトークンが使えるわけではない。Access Token には有効期限がある(Cognito のデフォルトは60分)。期限切れ後もユーザーに再ログインを強いるのは論外なので、Refresh Token を使ったサイレントリフレッシュが必要だ。

リフレッシュの基本フロー

swift
// TokenClient 内 func refreshAccessToken(refreshToken: String) async throws -> TokenResponse { var request = URLRequest(url: cognitoTokenURL) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") let body = "grant_type=refresh_token&client_id=\(clientId)&refresh_token=\(refreshToken)" request.httpBody = body.data(using: .utf8) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { // Refresh Token が無効化されている場合、再ログインを促す throw AuthError.refreshTokenExpired } return try JSONDecoder().decode(TokenResponse.self, from: data) }

Cognito における Refresh Token の寿命設定

Cognito User Pool の設定で Refresh Token の有効期限は1日〜3650日の範囲で指定できる。ここで考慮すべきは以下の2点だ。

  • 短すぎる(例: 1日): ユーザーが毎日再ログインを強いられ、UXが著しく悪化する。
  • 長すぎる(例: 365日): 退職者のトークンが1年間有効なままになるリスク。

エンタープライズでは 7〜30日 が現実的な落とし所だ。ただし、Azure AD 側でユーザーを無効化した場合でも、既に発行済みの Cognito の Refresh Token は即座には無効にならない点に注意が必要だ。これを補うには、バックエンド API 側で ID Token 内の subemail を使って、独自のユーザー無効化チェックを併用することを推奨する。


8. エラーハンドリングの急所

認証フローは「正常系が動いて当たり前」だが、異常系の設計こそがシニアエンジニアの真価を問われる場面だ。

8.1. 認可コードの期限切れ・二重使用

Cognito の認可コードは一度きりかつ数分で失効する。以下のケースで invalid_grant エラーが返る。

  • ネットワーク遅延でトークン交換リクエストが遅れた
  • ユーザーが戻るボタンを押して、古い認可コードで再度交換を試みた

対処法はシンプルだ。invalid_grant を受けたら、ユーザーに「セッションの有効期限が切れました」と表示し、最初からフローをやり直させる。中途半端なリトライは逆にバグの温床になるため避けること。

8.2. SAML Response の署名検証失敗

Azure AD の証明書ローテーション時に発生しやすい。Azure AD は定期的に署名用証明書を更新するが、Cognito 側のフェデレーション設定で古い証明書のメタデータが残っていると、SAML Response の署名検証に失敗し、ユーザーはログインできなくなる。

これを防ぐには、Cognito の SAML IdP 設定で「メタデータURL」を指定する(メタデータファイルの直接アップロードではなく)。メタデータURLを設定しておけば、Azure AD が証明書を更新した際にCognitoが自動的に最新のメタデータを取得してくれる。

メタデータURL例:
https://login.microsoftonline.com/{tenant_id}/federationmetadata/2007-06/federationmetadata.xml

8.3. ネットワーク断裂時のリトライ戦略

トークン交換(/oauth2/token への POST)は冪等ではない。認可コードは一度しか使えないため、「リクエストは送られたがレスポンスを受信できなかった」場合のリトライは invalid_grant を引き起こす。

したがって、トークン交換リクエストに対しては自動リトライを行わないのが正解だ。代わりに、エラー発生時は認証フロー全体を最初からやり直す設計とする。一方で、Refresh Token によるトークンリフレッシュは冪等であるため、Exponential Backoff によるリトライが安全に行える。


9. 総括

SAMLとOAuth2.0/OIDCの境界をまたぐ本アーキテクチャにおいて、最も重要なのは**「どの情報が、どのネットワーク経路を通り、誰がそれを検証するのか」**というトラストバウンダリ(信頼境界)の意識である。

  • SPAはただの土管である。 認証の主導権とトークン要求権は、PKCEを用いてiOSネイティブアプリに独占させること。
  • Custom URL Scheme にトークンを乗せない。 横取りされても無害な「認可コード」のみを運搬すること。PKCEと state パラメータを併用し、コード横取りとCSRFの双方を防ぐこと。
  • SLO(ログアウト)の沼にはまらない。 複雑な IdP-SP 間のログアウト連鎖を追うのではなく、Token Revocation と Ephemeral Web Session の組み合わせによって、セキュアで確実な「未ログイン状態」を強制的に作り出すこと。
  • 異常系を甘く見ない。 認可コードの一回性、SAML証明書のローテーション、トークン交換の非冪等性を理解し、「失敗したらフローごとやり直す」という割り切りを設計に組み込むこと。

これらの要所を抑えれば、Cognito と Azure AD という強力なピースを用いた認証基盤は、極めて堅牢に機能するはずだ。