有名テック企業の技術ブログを、ひとつのフィードで。
フィード
34件
こんにちは。 大学の春休みにPlatform Engineering Unit Identity Platformグループでインターンをしていた河内です。 今回のインターンでは、Sansan株式会社の複数プロダクトに認証・認可を提供している共通認証基盤「Auth One」へRefresh Token Rotationの追加に取り組みました。 背景となるプロダクト側のMCP提供については、次のリリースも参照ください。 Contract One、MCPサーバーの提供を開始(プレスリリース) Refresh Token Rotation自体は、OAuth/OIDCに関わる人にとっては比較的知られた考え方かもしれません。一方で、実際にプロダクトの共通認証基盤へ組み込むには、仕様、既存実装との互換性、セキュリティー、UX、運用時の観測性を同時に考える必要がありました。 本記事では実装の細部ではなく、導入にあたって行った設計判断を中心にまとめます。 Auth Oneとは Auth Oneは、Bill OneやContract Oneなどのプロダクトに対して認証・認可を提供する共通認証基盤です。 OAuth/OIDCプロバイダーとして、パスワード認証、SSO、API認可などを提供しています。もともとはAuth0のコスト課題を契機に、Amazon Cognitoとサーバーレスアーキテクチャを中心に内製化された基盤です。 Amazon Cognitoや利用しているライブラリで要件を満たせない部分については、Auth One側で追加実装しています。 参考:Sansanの認証基盤を支えるアーキテクチャとその振り返り (Speaker Deck) なぜRefresh Token Rotationが必要だったのか 背景はMCP(Model Context Protocol)対応です。 Sansanでも、MCPサーバーを通じてプロダクトの機能やデータをAIアプリケーションやAIエージェントから利用できるようにする取り組みが進んでいます。このときAuth OneはMCPサーバーのOAuth認可基盤として利用されます。 MCPクライアントがMCPサーバーを継続的に利用するには、認可後もAccess Tokenを更新し続ける必要があります。そこでRefresh Tokenを利用しますが、MCPクライアントの形態上、Refresh Tokenを安全に扱ううえでいくつか課題があります。 今回想定したMCPクライアント(ユーザー環境で動作する形態)ではClient Secretを安全に保持できないため、 Public Clientとして扱う必要がある 一般にPublic ClientでRefresh Tokenを使う場合、漏えい時の不正利用リスクを下げるために (1) Sender-constrained(DPoPなど)とする、または (2) Refresh Token Rotationを行う、といった対策が必要。Auth OneはDPoPを実装済みだが、2026/05現在のMCP仕様ではDPoPが採用されておらず、MCPクライアントでは (1) を前提にできない。 Roadmap: https://modelcontextprotocol.io/development/roadmap#on-the-horizon SEP-1932(DPoP): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1932 特にPublic Clientでは端末内での保護に限界があり、さらにDPoPなどでトークンを特定クライアント(鍵)に束縛(sender-constrain)できない場合、Refresh Tokenが単体で漏えいしただけで第三者による継続的な不正利用につながり得ます。 この条件下でRefresh Token漏えい時の影響範囲を抑えるため、Refresh Token Rotationを導入する必要がありました。 Refresh Token Rotationと、その単純実装で起きる問題 Refresh Token Rotationは、Refresh Tokenを使ってAccess Tokenを更新するたびに、Refresh Token自体も新しいものへ入れ替える仕組みです。 通常のRefresh Tokenでは、漏えいしたトークンが有効期限まで使われ続ける可能性があります。一方、Refresh Token Rotationでは、古いRefresh Tokenを使った時点で新しいRefresh Tokenへ入れ替えるため、漏えいしたRefresh Tokenが使われ続けるリスクを抑えやすくなります。 ただし、単純に旧Refresh Tokenを失効し、新Refresh Tokenを発行するだけだと、クライアントが不必要に再ログインを求められる状況が起こりえます。 代表例は次の2つです。 レスポンス未達によるトークン喪失 サーバー側では旧Refresh Tokenを無効化し、新Refresh Tokenを発行したにもかかわらず、ネットワークエラーなどでレスポンスがクライアントへ届かない場合、クライアントは旧Refresh Tokenしか保持していません。 その結果、次回以降の更新が失敗し、再ログインが必要になることがあります。 同時リフレッシュによる競合 同じRefresh Tokenを使ってほぼ同時にリフレッシュが走ると、一方が成功してローテーションされた後、もう一方は旧Refresh Tokenでのリクエストになり失敗しうるため、再ログインにつながることがあります。 この正しく更新しているのに再ログインとなる問題をどう吸収するかが、設計上の主要な論点でした。 設計判断1: Grace Period方式か、冪等方式か Refresh Token Rotationの実装方針はRFCで細かく規定されているわけではないため、既存の認証基盤・サービスの実装方針を調査しました。 調査した範囲では、レスポンス未達や同時リフレッシュへの対処は大きく2つに分かれました。 方式 概要 特徴 Grace Period方式 ローテーション後も、旧Refresh Tokenを一定時間受け付ける 実装は比較的素直だが、同じ旧Refresh Tokenから複数の新Refresh Tokenが発行されうる 冪等方式 同じ旧Refresh Tokenに対するリクエストには、同じ新Refresh Tokenを返す 有効なRefresh Tokenを常に 1 つに保ちやすいが、新Refresh Tokenを一時保存する必要がある Auth Oneでは冪等方式を選んだ Auth Oneでは最終的に冪等方式を選びました。 なお冪等方式では、同じ旧Refresh Tokenに対して同じ新Refresh Tokenを返すことで再送や同時リフレッシュによる競合を吸収しますが、無期限に同一応答を保証するのではなく、一定期間に限って同一応答を返す設計にすることが多いです。本記事ではこの期間を「冪等Window」と呼びます。 冪等方式を選んだ理由は次の2つです。 トークン管理を単純に保ちやすい Grace Period方式では、同時リフレッシュ時などにRefresh Tokenが分岐します。そのため、有効なRefresh Tokenの系列が木構造のように増えていき、管理が複雑になります。 冪等方式では、同じ旧Refresh Tokenに対して同じ新Refresh Tokenを返すため、分岐を避けやすくなります。 冪等Window内の再リクエストでDB書き込みを避けやすい 再リクエスト時にキャッシュから同じ新Refresh Tokenを返せれば、毎回のDB書き込みを伴わずに処理できます。 設計判断2: 新Refresh Tokenをどう安全に一時保存するか 冪等方式では、同じ旧Refresh Tokenに対して同じ新Refresh Tokenを返すために、新Refresh Tokenを一時的に保存する必要があります。 ここで問題になるのは、Refresh Tokenが長寿命であり、新しいAccess Tokenを発行する起点になる重要な値だという点です。 Auth Oneの既存実装では、Refresh Tokenの値そのものは保存せず、一致確認用のハッシュのみを保存していました。これは、Refresh Tokenの漏えいリスクを下げるためです。 そのため、冪等方式のためだけに新Refresh Tokenを平文でキャッシュする設計は避けたい、という前提がありました。 検討した選択肢は、主に次のようなものです。 選択肢 概要 懸念 平文で保存する 透過的データ暗号化に任せ、Valkey には平文で保存する アプリケーションからはRefresh Tokenを平文で扱える 共通鍵で暗号化する アプリケーション共通の鍵で新Refresh Tokenを暗号化する 鍵漏えい時の影響範囲が大きい。追加で鍵管理する必要がある。 KMSでエンベロープ暗号化する KMS を使ってデータキーを保護する レイテンシやコストが増える 旧Refresh Token由来の鍵で暗号化する 旧Refresh Tokenから鍵を導出し、新Refresh Tokenを暗号化する 鍵導出の安全性(用途の分離や漏えい時の影響)を設計で担保する必要がある HMACなどで導出する 保存ではなく導出で対応する 前方秘匿性などの観点で課題がある この中で採用したのが、旧Refresh Tokenから鍵を導出し、その鍵で新Refresh Tokenを暗号化して保存する方式です。 旧Refresh Tokenから鍵を導出して新Refresh Tokenを暗号化する 採用した方式のポイントは、冪等キャッシュヒット時のリクエストには、必ず旧Refresh Tokenが含まれることです。 同じ旧Refresh Tokenに対して同じ新Refresh Tokenを返すには、クライアントが旧Refresh Tokenを再度送ってくる必要があります。つまり、復号に必要な材料である旧Refresh Tokenは、リクエストから毎回得られます。 そこで、旧Refresh TokenからHKDF-SHA256で256bitの対称鍵を導出し(用途分離のためにinfoを設定)、その鍵で新Refresh TokenをAES-256-GCMにより暗号化します。 Valkeyには、新Refresh Tokenの平文ではなく、暗号文、IV、認証タグを保存します。キャッシュキーは旧Refresh Tokenのハッシュを用いて、旧Refresh Tokenを平文で扱う箇所を増やさないようにしました。 なお、この方式が主に想定しているのは、キャッシュ(Valkey)や周辺コンポーネントから暗号文が漏えいした場合の影響を抑えることです。旧Refresh Token自体が漏えいしているケースは別問題であり、その前提ではRefresh Token Rotationや有効期限など別の対策が重要になります。 初回リフレッシュ時の流れは次の通りです。 クライアントが旧Refresh Tokenでリフレッシュを要求する Auth Oneが旧Refresh Tokenを検証する 新Refresh Tokenを生成する 旧Refresh TokenからHKDF-SHA256で鍵を導出する 新Refresh TokenをAES-256-GCMで暗号化する 暗号文、IV、認証タグをValkeyに短時間保存する クライアントへ新Access Tokenと新Refresh Tokenを返す sequenceDiagram participant C as クライアント participant A as Auth One participant D as DB participant V as Valkey C->>A: 1. Refresh Request (旧RT) A->>D: 2. 旧RT 検証・無効化 A->>D: 3. 新RT保存 A->>V: 4. 暗号化キャッシュ設定 A-->>C: 5. 新RT + 新AT 返却 その後、冪等Window内に同じ旧Refresh Tokenで再リクエストが来た場合は次の通りです。 クライアントが同じ旧Refresh Tokenでリフレッシュを要求する Auth OneがValkeyを参照する キャッシュヒットした暗号文を取得する リクエストに含まれる旧Refresh Tokenから同じ鍵を導出する 暗号文を復号し、新Refresh Tokenを取得する 同じ新Refresh Tokenと、新たに生成したAccess Tokenを返す sequenceDiagram participant C as クライアント participant A as Auth One participant V as Valkey C->>A: 1. Refresh Request (旧RT) A->>V: 2. Valkeyキャッシュ検索 → HIT A->>A: 3. HKDF(旧RT)で復号 <br> → 新RT取得 + 新AT再生成 A-->>C: 4. 同じ新RT + 新AT 返却 この方式では、Valkeyに新Refresh Tokenの平文を保存しません。また、復号用の共通鍵を別途永続管理する必要もありません。 情報セキュリティ部レビューとパラメータ設計 Refresh Tokenは認証基盤において重要な値です。そのため、設計を確定する前に情報セキュリティ部へ相談しました。 相談時には、次のような点を整理しました。 なぜRefresh Token Rotationが必要なのか なぜ冪等方式を選ぶのか 新Refresh Tokenをどこに、どの形式で保存するのか 旧Refresh Token由来の鍵で暗号化する方式にどのようなリスクがあるか 想定する攻撃や漏えい時の影響範囲 各種有効期限やWindowをどう設定するか また、パラメータは最初から最適値を決め打ちするのではなく、観測しながら調整できるようにしました。 これらの値は、セキュリティーとUXの両方に影響します。期間を短くすれば、漏えい時の影響範囲は小さくなります。一方で、短すぎるとクライアントが頻繁に再認証を求められ、UXが悪化します。逆に期間を長くすればUXは改善しますが、漏えい時のリスクは大きくなります。 実装と検証 実装では、主に次の作業をしました。 DB設計とマイグレーション Protocol Buffersでのスキーマ設計 内部向けAPI対応 Refresh Token Rotationのコアロジック 暗号化・復号ロジック E2Eテスト OpenTelemetryメトリクス k6による負荷テスト 実MCPクライアントでの動作確認 検証では、主要サービス3種のMCPクライアントを使い、Product側のMCPサーバーへ接続しました。 MCPクライアントがMCPサーバーへ接続し、Auth Oneを通じてOAuth認可とトークンの更新を行います。MCPサーバー側では取得したJWTを検証してサービス環境へアクセスします。 検証の結果、3種類のクライアントで認証・接続が成功し、定期的なRefresh Token更新を通じて継続利用できることを確認しました。 また、テスト・観測面では次の観点を確認しました。 種別 確認したこと ユニットテスト 暗号化・復号、ローテーション処理 E2Eテスト Refresh Tokenの連続ローテーション、冪等性 負荷テスト 想定されるリフレッシュ頻度での挙動 メトリクス キャッシュヒット、ローテーション結果、Refresh Tokenの経過秒数、セッション全体の経過秒数 実クライアント検証 ChatGPT、Claude Desktop、Copilot Studioでの接続 やりきれなかったことと今後 今回のインターン期間では、Refresh Token Rotationの基本的な設計、実装、テスト、実クライアント検証まで進めることができました。 一方で、今後取り組むべきことも残っています。 Refresh Token Rotationの適用範囲の拡大 管理画面からRefresh Token Rotationを有効化できるようにする対応 実際の利用状況を見ながらの各種パラメータ調整 おわりに 今回の取り組みでは、Refresh Token Rotationという一見よく知られた機能であっても、実際にプロダクトの共通認証基盤へ組み込むには多くの判断が必要だと感じました。 特に、MCPクライアントのようにPublic Clientであり、かつDPoPを前提にできないクライアントでは、仕様上の要件、セキュリティー、UX、既存基盤との互換性を同時に考える必要があります。 また、OAuth/OIDC周りでは、仕様に明確に書かれている部分と、実装側で判断しなければならない部分があります。今回のRefresh Token Rotationもまさにそのような領域でした。「銀の弾丸」はなく、複数の選択肢のPros/Consを比較しながら、その時点の要件に対して最も妥当な方式を選ぶことが重要だと学びました。 最後に
こんにちは。情報セキュリティ部 Product Securityグループの北澤です。 昨年に引き続き、新卒エンジニア向けに研修を行いました。この記事では、本年度の研修内容についてお伝えします。 研修について 昨年と同じく、丸一日かけて実施しました。 研修スケジュール 基礎編では、Web、インフラやモバイルなどの領域を問わず必要な情報セキュリティーの基礎に関する講義を行いました。午後にはSQL InjectionやXSSなどをはじめとしたWebアプリケーションに関する攻撃手法とその防衛について講義を行いました。 講義内容 基礎編 午前中は基礎編の講義を行いました。 最初にそもそも「情報セキュリティ」とは何か? から始まり、対策をしない場合はどのような被害が考えられるのか? Webサービスを公開していると実際にどの程度攻撃が行われるのか? ということをお話ししました。 そこから、対策するためには何を知るべきか? 脆弱性とは何か? 脆弱性情報を渡されたときにリスクと対応優先度をどう考えればいいのか? という実際の現場で直面するだろう課題と考え方について講義を行いました。 また、Claude CodeやCodexなど、AIエージェントの活用が加速している現状を鑑みて、昨年の内容に加え、AI周りのセキュリティについても触れるようにしました。内容としてはローカルでAIエージェントでコーディングを行う際に注意すべき点、プロダクトにAI機能を組み込む際に気を付けるべき点の二つについて話をしました。 いくつか資料を紹介します。 情報セキュリティとは 機密性についてのスライド 可用性についてのスライド 対応優先度を考える上で大切な項目 深層防御/多層防御 を説明するためのたまねぎ Web編 午後はインジェクションなどWebサービスに埋め込んでしまいやすい問題について、項目ごとに脅威・攻撃手法・対策についての講義を行いました。具体的には以下です。 SQL Injection XSS 認証不備 認可不備 SSRF CSRF 設計に関する問題 コンポーネント(ライブラリなど)に関する問題 ログに関する問題 暗号化の不備 SQL Injectionの概要の説明 SQL Injectionの影響と対策について XSSの概要の説明 昨年との違いとしては、内容に応じてローカルでハンズオン用アプリケーションを動かしてもらい、実際に講義ごとに攻撃を体験してもらうようにしました。 SQL Injectionのハンズオン操作説明スライド ハンズオンアプリケーションに攻撃ペイロードを送信しようとしている画面 攻撃ペイロード送信によって、パスワード無しでadminにログインできてしまった状態 裏話ですが、ハンズオンのアプリケーションは実験的にAspireを使い、開発や立ち上げが楽になるようにしてみました。SSRFのような複数アプリケーションを扱うものや、SQL Injectionのようにアプリ+postgresというDBコンテナを扱うものに対してはかなり開発体験が良かったですね。 aspire.dev 新卒のメンバーにアナウンスする際も、「Hostプロジェクトディレクトリでdotnet runしてね」で済むのでめちゃくちゃ楽でした。 CSRFは実際に攻撃されることの体験をして欲しいなと思っていたので脆弱な簡易SNSサービスと罠サイトである出席確認サイトを用意してみました。 簡易SNSサービスにログインしてもらった状態で、罠である「出席確認」のボタンを押してもらい、裏でスクリプトが動き、「ざわさんにごはんおごります!」という意図しない書き込みが簡易SNSに対して行われることを体験してもらいました。(ざわさんとは講師である私のことです) CSRF体験用の簡易SNS 簡易SNSにログインした状態で出席ボタンを押すと 罠サイトの出席確認システム画面 講師にごはんをおごる内容がSNSに投稿される。新卒のみなさん、ごちそうさまです。 ざわさんにごはんおごります!が投稿されます。 「やられた~」という声もあがり、実際にCSRF脆弱性を作り込んでしまったときの怖さをわかっていただけたかなと思います。 脆弱性について話したあとに、脆弱性を防ぐ・発見するためのセキュアな開発プロセスについて講義を行いました。 セキュアな開発プロセスの大事さについての説明スライド 設計時には設計時の、実装時には実装時に必要な対策をする、といった内容です。 設計時の問題がリリースギリギリになってわかるものほど悪夢的なことはないので、それを防ぐためにそれぞれの工程ごとにどのような検証を行う必要があるかについて話しました。 CTF Web編の後には楽しみながら実践的に知って記憶をしてもらうためのCTF(Capture the Flag)を行いました。昨年と同じく、攻撃対象のアプリケーションはASP.NET Coreで自前で用意し、スコアサーバにはCTFdを利用しました。 簡単なSQL InjectionからBlind SQL Injection、XSSによる管理者セッション窃取やjwtのalg:noneまでとさまざまな難易度の問題を用意し、挑んでもらいました。 CTFでは実際の攻撃者の動きを学んでもらうことを心がけました。 例えば、XSSではただアラートを表示させるなどではなく、実際に管理者にその画面を送信し、管理者のセッションを奪取してログインするという、より本格的な導線を用意していました。 CTFのXSS1問題 以下ブログ用画像のためにローカルで動かしています。 サンドイッチショップの検索バーでalert()を表示させるスクリプトを記述している <figure class="figure-image figure-image-fotolife" title=