本記事では匿名化されたエンタープライズ実装について説明します。企業名、内部ドメイン、リポジトリ識別子、チケット番号、および独自の管理名は、意図的に削除または一般化されています。
多要素認証は、しばしばログインの問題として説明されます。パスワードを入力し、コードを受け取り、本人確認を行う。
本ケーススタディで説明するシステムでは、そのモデルでは十分ではありませんでした。
この製品は、1 つのタブレットを取引中に複数の人が使用する店舗環境で動作していました:
- プロセスを開始する従業員;
- 承認またはサポートするマネージャー;
- 自分のデバイスで確認・署名する顧客。
課題は、ユーザーが 6 桁のコードを知っていることを証明することだけではありませんでした。共有する店舗セッションと顧客の個人用電話の間で、セキュアで短命な引き渡しを作成し、下流の署名セッションが漏洩したり、複数のデバイスが同じ取引を要求したりすることを防ぐ必要がありました。
この投稿では、そのプラットフォームのアーキテクチャ、セキュリティモデル、トレードオフ、および運用プラクティスについて説明します。
実際の問題:セキュアなデバイス引き渡し
ワークフローは共有タブレットで開始されました。ある時点で、顧客は自分の電話でプロセスの一部を継続する必要がありました。
そのためプラットフォームは、いくつかの質問に答える必要がありました:
- 電話は、従業員の前に立っている顧客のものだとどのように証明するのか?
- 共有タブレットは、正しい電話が正しいセッションを要求したことをどのように知るのか?
- QR コードが 2 回スキャンされた場合はどうなるのか?
- セッション識別子やトークンが URL、ブラウザ履歴、ログ、リファラヘッダーに表示されないようにするにはどうすればよいか?
- 検証に成功したことを電話に即座に通知するにはどうすればよいか?
- 単一用途の署名 URL が検証前に露出しないようにするにはどうすればよいか?
これらの制約により、一見小さな MFA 機能が、アイデンティティ、リアルタイム通信、並行性、エッジ配信、インフラストラクチャ、運用制御に関わる分散システムの問題に変わりました。
ハイレベルアーキテクチャ
このソリューションでは、2 つの主要なアプリケーションコンポーネントを使用しました:
- 顧客の電話用の React および TypeScript シングルページアプリケーション;
- AWS AppSync、Lambda、DynamoDB で構築されたサーバーレス GraphQL API。
インフラストラクチャには、CloudFront、S3、API Gateway、Route 53、ACM、WAF、Terraform、中央集権型ロギング、APM、合成監視、自動デプロイワークフローも含まれていました。
┌─────────────────────────────┐
│ Shared in-store tablet │
│ Employee / manager workflow │
└──────────────┬──────────────┘
│ create handoff session
▼
┌─────────────────────────────┐
│ GraphQL API │
│ AppSync + Lambda │
└──────────────┬──────────────┘
│ persist state
▼
┌─────────────────────────────┐
│ DynamoDB │
│ session + short-link data │
└─────────────────────────────┘
Shared tablet displays QR
│
▼
┌─────────────────────────────┐
│ Customer phone │
│ React SPA via CloudFront │
└──────────────┬──────────────┘
│ claim + subscribe
▼
┌─────────────────────────────┐
│ GraphQL API │
│ verification + status push │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode
アーキテクチャは意図的にサーバーレスにしました。ワークロードはバースト性が高く、ユーザー体験は短く、ほとんどの操作は長時間実行される計算ではなく小さな状態遷移でした。
エンドツーエンドのセッション引き渡しフロー
1. 引き渡しセッションの作成
店舗内アプリケーションは、取引コンテキスト、下流の署名 URL、有効期限データ、および PENDING ステータスを含む短命の引き渡しレコードを作成しました。
署名 URL は機密情報かつ単一用途として扱われ、引き渡しセッションが保留中である間は顧客向けアプリケーションに返されませんでした。
2. 短い QR リンクの生成
API は引き渡しレコードに関連付けられた短いコードを作成しました。店舗内タブレットはその短いリンクを QR コードとしてレンダリングしました。
顧客は自分の電話でそれをスキャンしました。
3. 電話セッションのセキュアなブートストラップ
QR リクエストは CloudFront と小さなリダイレクトエンドポイントを通過しました。
セッション識別子とトークンをクエリパラメータに配置する代わりに、リダイレクトは以下のように設定された短命の Cookie を返しました:
-
Secure; -
SameSite=Strict; - 非常に短い有効期限。
電話アプリケーションは値を読み取り、GraphQL クライアントを構成し、セッション引き渡しフローを継続しました。
これにより、以下を通じて認証情報が露出することを防ぎました:
- ブラウザ履歴;
- アクセスログ;
- コピーされた URL;
- 分析ツール;
- リファラヘッダー。
4. 引き渡しセッションの要求
電話は claimSession ミューテーションを呼び出しました。
バックエンドは以下を生成しました:
- 6 桁のセキュリティコード;
- 一意のセッション識別子。
コードは顧客の電話に表示され、共有タブレットを使用して従業員に読み上げられました。
プラットフォームは 最後にスキャンした者が優先 モデルに従いました。別の電話が同じ QR コードをスキャンした場合、新しい要求が以前のものを上書きしました。
これにより、2 台のデバイスが同じアクティブな引き渡しセッションを保持することを防ぎました。
5. コードの検証
従業員は 6 桁のコードを店舗内アプリケーションに入力しました。
バックエンドは検証を状態遷移として評価しました:
- 正しいコード:
PENDING → VERIFIED; - 残り試行回数がある誤ったコード:試行回数のデクリメント;
- 最終的な誤った試行:
PENDING → CANCELED; - タイムアウト:
PENDING → EXPIRED。
これらの遷移は、読み取り-修正-書き込みシーケンスではなく DynamoDB の条件付き書き込みを使用しました。データベースが並行性のプリミティブになりました。
これは重要でした。2 つの検証リクエスト、または 2 つの競合する要求の両方が成功することはできないためです。
6. 結果のリアルタイムプッシュ
電話は GraphQL サブスクリプションを使用して引き渡しステータスの更新をサブスクライブしました。
検証が成功すると、AppSync は新しい状態を電話に即座にプッシュしました。
ポーリングループはありませんでした。
その後、電話は最終的な認証済み読み取りを実行しました。その時点でのみ、API は単一用途の署名 URL を公開しました。
アプリケーションは顧客を署名体験にリダイレクトし、引き渡しレコードを期限切れにしました。
ポーリングではなく GraphQL サブスクリプションを使用する理由
ポーリングの方が説明しやすかったかもしれませんが、不要なトラフィック、遅いフィードバック、ライフサイクルのエッジケースを増やすことになりました。
製品要件は本質的にイベント駆動型でした:
店舗内セッションがこの正確な引き渡しセッションを検証したときに、この電話に通知する。
GraphQL サブスクリプションは以下を提供しました:
- 低遅延の状態変更;
- 引き渡しコンテキストによる組み込みフィルタリング;
- クライアントライブラリを通じた管理された WebSocket 再接続;
- カスタムポーリング間隔や再試行スケジューラが不要。
フロントエンドは、モバイル特有の障害モードを 1 つ処理しました。ユーザーが電話をロックしたり、アプリケーションを切り替えたりすると、オペレーティングシステムがブラウザタブを頻繁に一時停止します。
タブが再度表示されたとき、アプリケーションは引き渡し状態を再取得しました。これにより、WebSocket が一時停止され、最後のイベントを見逃した場合に対応できました。
6 桁のコードが果たした 2 つの目的
最も重要な設計判断の 1 つは、6 桁の値を次の両方として使用することでした:
- 人間が読める MFA チャレンジ;
- 引き渡し状態へのアクセスに必要なサーバーサイドの機能。
コードは読み取りとサブスクリプションフィルタの両方に必要でした。上書きされたデバイスには返されず、状態が VERIFIED に達するまで下流の署名 URL は非表示のままになりました。
これにより、2 つ目の不透明なセッショントークンの必要性がなくなり、フィールドレベルの認可を維持できました。
また、生成、保存、同期、無効化が必要な認証情報の数も削減できました。
階層化されたセキュリティモデル
プラットフォームは、MFA コードを唯一の制御手段として扱うのではなく、深度防御を使用しました。
エッジ保護
CloudFront とリージョンエンドポイントは AWS WAF で保護されました。管理ルールグループは段階的に導入され、高リスクのルールが最初に強制され、誤検知が発生しやすいグループは当初モニタリングモードのままにされました。
トランスポートとアイデンティティ
すべてのトラフィックで HTTPS を使用しました。GraphQL API は、カスタム Lambda オーソライザーを使用して OAuth/JWT トークン、発行者、署名、有効期限、操作レベルのスコープ、および認可ウェイトを検証しました。
未知の操作は失敗で終了しました。
コントラクトの強化
GraphQL サーフェスは、デプロイ環境でイントロスペクションを無効にし、クエリ深度とリゾルバ数を制限しました。
フィールドレベルの認可
リゾルバは、引き渡し状態を返す前にセキュリティコードをチェックしました。
署名 URL は、引き渡しセッションが検証された場合にのみ含まれました。
上書きされたサブスクリプションの場合、リゾルバはセッションが変更された理由を明かさず、データを返しませんでした。
認証情報の衛生管理
機密性の高い引き渡し認証情報は URL に渡されませんでした。ログには個人を特定できる情報や認証シークレットは含まれませんでした。相関識別子はセッションデータを漏洩させずにデバッグ用に保持されました。
フロントエンドを意図的に小さく保つ
顧客向けアプリケーションは単一目的のフローであったため、フロントエンドは不要なフレームワークの複雑さを避けました。
使用した技術:
- React;
- strict モードの TypeScript;
- Vite;
- React Context とローカルフック;
- 管理された GraphQL クライアント;
- 共有エンタープライズデザインシステム。
ルーターやグローバルステートライブラリはありませんでした。
アプリケーションにはわずかな状態しかありませんでした:
- 読み込み中または要求中;
- MFA コードの表示;
- エラーまたは終了ステータスの表示;
- 検証後のリダイレクト。
この単純さにより、バンドルサイズ、アタックサーフェス、アップグレード負担、認知負荷が低減しました。
初日から Infrastructure as Code
すべてのインフラストラクチャは Terraform で定義されました。
API リポジトリは、バックエンドだけでなくフロントエンドに必要な共有インフラストラクチャもプロビジョニングしました:
- AppSync;
- Lambda 関数;
- DynamoDB;
- S3;
- CloudFront;
- API Gateway;
- DNS と証明書;
- WAF;
- 監視リソース。
フロントエンドリポジトリは不変のビルドアーティファクトを生成しましたが、別のインフラストラクチャスタックを所有しませんでした。
これにより、アプリケーションと API が独立して進化できる一方で、プラットフォームトポロジの単一の信頼できる情報源が作成されました。
ビルドは 1 回、デプロイは複数回
フロントエンドはビルド時に環境固有の設定を埋め込みませんでした。
代わりに、デプロイされたホスト名から環境を導出しました。そのため、同じアーティファクトを変更せずに開発、QA、プレプロダクション、本番環境に昇格できました。
このパターンには 2 つの利点がありました:
- 本番環境には、以前にテストされた正確なアーティファクトが届く;
- 設定はビルドの懸念ではなく、デプロイの懸念のままになる。
配信パイプラインには以下が含まれました:
- リンティングと自動テスト;
- 静的解析;
- インフラストラクチャ検証;
- セキュリティスキャン;
- セマンティックバージョニング;
- 不変アーティファクトの発行;
- 環境承認;
- 本番変更管理の統合。
可観測性は製品の一部
運用上の可視性は、ローンチ後に追加されたものではありませんでした。
プラットフォームには以下が含まれました:
- 構造化 JSON ログ;
- 相関識別子;
- 名前付きビジネスイベント;
- 中央集権型ログ転送;
- APM 計装;
- Lambda、GraphQL、トランザクション健全性のダッシュボード;
- レイテンシ、エラーレート、スロットルアラート;
- 合成ヘルスチェック;
- 環境固有のエスカレーションルール。
監視設定は独立した Terraform ステートに存在したため、メインのアプリケーションインフラストラクチャを変更せずに変更できました。
この分離は、アラートがアプリケーションリソースとは異なるペースで進化することが多いため有用でした。
実際のトレードオフ:安価な API オプションよりコンプライアンスを優先
より参考になる判断の 1 つは、API Gateway に関するものでした。
最初の実施では、よりシンプルで安価な新しい HTTP API を使用しました。コンプライアンス作業中に、必要な WAF 関連付けがその API タイプではサポートされていないことが判明しました。
利用可能な選択肢は次のとおりでした:
- 新しい API を維持し、例外を要求する;
- エッジフローを再設計する;
- 必要な WAF アタッチメントをサポートする REST API v1 に移行する。
プラットフォームは REST API v1 に移行しました。
これによりリクエストあたりのコストは増加しましたが、観測されたトラフィックは低く、月次への影響は無視できる程度でした。セキュリティとコンプライアンスの利点は、小さなインフラストラクチャ節約よりも重要でした。
教訓は、REST API v1 が常に優れているということではありません。教訓は、開発者の利便性や見出しの価格だけでなく、完全な本番制約セットに対してクラウドサービス選択を評価することでした。
垂直スライスによるデリバリー
製品は、空のレポジトリから本番環境へ、段階的なフェーズを通じて移行しました:
- 基盤となる API、データベース、Terraform モジュール;
- エッジ配信、DNS、証明書、最初のモバイル UI;
- カスタム認可とスコープ付きアクセス;
- 有効期限、短いリンク、QR リダイレクト、セキュアなセッションブートストラップ;
- 排他的な要求とリアルタイムサブスクリプション;
- 構造化ロギング、ダッシュボード、アラート、合成監視;
- WAF 強制とコンプライアンスロールアウト;
- 本番自動化とライフサイクルメンテナンス。
各フェーズは、切り離されたファイルのセットではなく、完全なアーキテクチャ機能を提供しました。
これにより、統合リスクが早期に可視化され、プラットフォーム制約が発見されるにつれて設計を進化させることができました。
再利用に値するパターン
小規模な分散ステートマシンに条件付き書き込みを使用する
単純な遷移を持つワークフローでは、DynamoDB の条件式でアプリケーションロックや読み取り前書き込みロジックを置き換えることができます。
却下された設計を価値あるドキュメントとして扱う
いくつかの実装パスは、プラットフォーム制約のために失敗しました。それらが失敗した理由を記録することで、将来のエンジニアが同じ実験を繰り返すことを防ぎました。
認可を可能な限りデータに近づける
リゾルバ内のフィールドレベルチェックにより、クエリとサブスクリプションの両方が同じルールに従うことを保証しました。
セキュリティ制御を段階的に提供する
WAF 設定はモニタリングモードで導入され、分析された後、ロールバックフラグと本番カナリアとともに選択的に強制されました。
単一目的のフロントエンドを単一目的に保つ
ルーターや複雑なステートライブラリの不在は、意図的なアーキテクチャ上の選択であり、洗練度の欠如ではありません。
初期アーキテクチャに本番対応を組み込む
認証、CI/CD、可観測性、セキュリティスキャン、ヘルスチェック、ロールバックメカニズムは、機能完成後に延期されるべきではありません。
最終的な要点
このプラットフォームで最も難しい部分は、6 桁のコードを生成することではありませんでした。
本当のエンジニアリング課題は、共有デバイスと顧客所有デバイスの間で信頼できる引き渡しを作成し、並行性、リアルタイムステータス、短命の認証情報、単一用途の下流セッション、本番セキュリティ、エンタープライズ配信制御を扱うことでした。
カスタム MFA フローは、小さな認証ウィジェットではなく、完全な分散システムとして扱われるときに価値を発揮します。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.