ID連携の標準化仕様紹介とセキュアな実装のためのアプローチ ~ 2021

f:id:ritou:20210904174248p:plain

おはようございます ritou です。
久々に「解説付きスライド全公開」的なやつをやります。

f:id:ritou:20210904174413p:plain

先月、チーム内でID連携のための標準化仕様に関する勉強会(私が一方的に話す会)を行いました。
が、実際はだいぶグダグダになってしまい、これはその後色々付け足してるうちに別物になってしまった資料です。

f:id:ritou:20210904200923p:plain

内容としては、ID連携のための標準化仕様にどのようなものがあるかを知ってもらうための「入門編」のような立ち位置で作りました。

f:id:ritou:20210904200944p:plain

OpenID Connect(SAMLのような) ID連携のための標準化仕様を紹介しようと思うと、ついつい個別にシーケンスやリクエスト/レスポンスの説明を始めがちですが、初学者が気になるのはそんな細けぇことではないでしょう。 まずは「この仕様で何ができるようになるのだろう」「この仕様では何を実現したいんだろう」と言うところから理解していくのが良いのではないでしょうか。

そこで、今回はこれらの仕様を知るきっかけとして、

  • たくさんある仕様のうち、"何かをできるようにするための仕様" の概要
  • "セキュリティ強化のための仕様" で使われていてID以外のWebアプリケーション開発に活かせそうな考え方

を紹介していきます。

ID連携機能を提供するための仕様とユースケース

f:id:ritou:20210905005548p:plain

前半は「こんなことを実現するための標準化仕様がありますよ〜」って言うお話をします。

OpenID Connect

f:id:ritou:20210905005709p:plain

いきなりですが、OpenID Connectの話を(ちょっと)します。

f:id:ritou:20210905012239p:plain

まずは、よくある「●●でログイン」って言う機能の流れを紹介します。

ステップ1として、Relying Party(以下、RP)という「外部アカウントを利用してログインさせるサービス」のログイン画面にて、ユーザーが「●●でログイン」と言うボタンを選びます。

f:id:ritou:20210905012422p:plain

ステップ2として、RPがOpenID Provider(OP)にユーザーを送ります。 画面のリダイレクトやポップアップなどで実現されます。

f:id:ritou:20210905012448p:plain

ステップ3,4では、OPがユーザーに対して認証を要求したり、「RPに対してあなたの情報を提供することを許可しますか?」と言って確認を求めます。

f:id:ritou:20210905012514p:plain

ステップ5では、ユーザー情報と合わせて「いつ、誰に対してどんな情報を提供することに同意を得たか」と言った認証イベントの情報が提供されます。

f:id:ritou:20210905012539p:plain

ステップ6では、RPは受け取った認証イベントの情報を検証して紐づくユーザーをログイン状態にします。

f:id:ritou:20210905012747p:plain

このような「●●でログイン」とう機能を実現するための標準化仕様がOpenID Connect(OIDC)であり、OPがユーザーの同意の元でRPに認証イベントの情報とユーザーの属性情報を提供するための仕組み です。

f:id:ritou:20210905012826p:plain

OIDCのコアとなる仕様ではここまで説明した最低限の機能である

  • 認証イベント情報の受け渡し方
  • ユーザーの属性情報の提供方法

が定義されています。

それ以外にもいくつか仕様があるのですが一部は後から紹介します。

OAuth 2.0

f:id:ritou:20210905013037p:plain

次に紹介するのが、OAuth 2.0です。

f:id:ritou:20210905013120p:plain

あるサービスが別のサービスのユーザー情報にアクセスする「データ連携」の流れを紹介します。

ステップ1として、ユーザーはブログのようにClientと呼ばれる「外部サービスのデータを利用するサービス」で、別サービスで管理するリソースの利用を開始します。(例えばログイン状態の設定画面の「●●と連携する」ような機能から処理が始まることが多いでしょう。)

f:id:ritou:20210905013749p:plain

ステップ2として、ClientがAuthorization Server(以下、AS)にユーザーを送ります。

f:id:ritou:20210905013813p:plain

ステップ3,4では、ASがユーザーに対して認証を要求してResource Orner(RO)を特定し、「Clientがあなたの情報にアクセスすることを許可しますか?」とリソースアクセスの許可を求めます。

f:id:ritou:20210905013847p:plain

ステップ5では、ASからClientに対してAPIアクセスのためのトークン発行処理が行われることでリソースアクセスの権限を提供します。

f:id:ritou:20210905013943p:plain

ステップ6ではClientが取得したリソースアクセスの権限を利用してAPIアクセスを実行できる状態、つまり「連携済み」の状態となっています。

f:id:ritou:20210905014208p:plain

このようなデータ連携を実現させるための仕様がOAuthです。 一言で説明すると、認可サーバー(AS)がリソースオーナー(RS)の同意のもと、クライアントに対してリソースアクセスを提供するための仕組み です。

OIDC vs OAuth 2.0

f:id:ritou:20210905014241p:plain

と、OIDCとOAuth 2.0の両方を並べて説明したところで "なんか似てる" と思われた方もいるでしょう。 登場人物の呼び名が違うものの、一連の流れは同じような感じでした。

f:id:ritou:20210905014327p:plain

で、この辺りの話をするときに避けられないのがいわゆる "OAuth認証"と言うワードです。

「データ連携を提供する仕組みであるOAuthを用いても、リソースオーナーの情報にたどり着くことができたらそのユーザーでログインさせられるよね。」

この考えがOAuth認証と呼ばれるものの正体です。

Client -> AS -> Clientという一連の流れでリソースアクセスを許可したリソースオーナー本人を特定してログインさせることができれば問題はありませんが、少しでもその流れが保証されないような仕組みになっていると問題が起こり得ます。

ritou.hatenablog.com

それに対して、OIDCではRPは一連のフローで取得した認証イベントの情報を利用してユーザーをログインさせます。少しだけ、この認証イベント情報について触れましょう。

f:id:ritou:20210905014404p:plain

OIDCの仕様を見たことがある方はIDトークンと呼ばれているものをご存知でしょうか。

  • いつ (トークン発行日時)
  • 誰が (ユーザー)
  • 誰に (RP)
  • 誰が持つ (OP)
  • どんな属性情報を提供することを (scope)

という5W1Hのような情報、あとはセキュリティ対策のためにRP -> OP -> RPと引き回される値などを含んだものが認証イベント情報です。

後は継続的にユーザーの属性情報を利用したいサービスのために専用のAPI仕様が定義してあるのがOIDCです。

ちょっと昔話をすると、この属性情報APIへのアクセスも独自に仕様策定が進められていましたが、仕様策定当時に同様に策定が進められていた OAuth 2.0 と目的が一致していたため、OAuth 2.0をベースとしてこのようなIDレイヤーに関する仕様を加えて拡張したものとして仕様策定を進めた という関係性になっています。

OIDCで扱う属性情報についても少しだけ触れておきましょう。

f:id:ritou:20210905014432p:plain

OIDCでは汎用的なプロフィール情報、あとはメールアドレスや電話番号は確認済みフラグの定義もあります。

f:id:ritou:20210905014457p:plain

最新の属性情報を返す "UserInfo Endpoint" から返されるJSONの表現例を載せておきます。 住所も細かく分かれてるのとか連結したやつとかが定義されています。

よくこの手のフォーマット見て 「日本にはな、「カナ」「漢字」と言うのがあるんじゃ。どうすりゃいいのよ。」 みたいなことがありますが、実はカタカナや漢字の氏名もサポートされています。

5.2. Claims Languages and Scripts

Human-readable Claim Values and Claim Values that reference human-readable values MAY be represented in multiple languages and scripts. To specify the languages and scripts, BCP47 [RFC5646] language tags are added to member names, delimited by a # character. For example, family_name#ja-Kana-JP expresses the Family Name in Katakana in Japanese, which is commonly used to index and represent the phonetics of the Kanji representation of the same represented as family_name#ja-Hani-JP. As another example, both website and website#de Claim Values might be returned, referencing a Web site in an unspecified language and a Web site in German.

(日本語訳)

Human-readable な Claim Value およびそれらへの参照は, 複数言語および複数文字種で表現することができる (MAY). 言語および文字種の指定には BCP47 [RFC5646] 言語タグを # を区切り文字としてメンバー名に付与する. 例えば family_name#ja-Kana-JP は日本語のカタカナ表記の Family Name を示す. カタカナ表記の氏名は, 漢字表記 (family_name#ja-Hani-JP) の氏名の発音を示したりインデックス目的等で用いられることが多い. 他にも website が言語指定のない Web サイト, website#de がドイツ語の Web サイトを参照していることを示すといった例も挙げられる.

  • family_name#ja-Kana-JP : カナ 姓
  • given_name#ja-Kana-JP : カナ 名
  • family_name#ja-Hani-JP : 漢字 姓
  • given_name#ja-Hani-JP : 漢字 名

Yahoo! JAPAN の属性取得API(UserInfoAPI)のドキュメントにも書いてあります。

developer.yahoo.co.jp

個人的にはこの辺りがあるおかげで普段個別で持ってたデータも汎用的に扱えて便利そうだなーと思っています。

f:id:ritou:20210905014528p:plain

ここまでざっとOIDCの概要について説明しました。 実際は関連する仕様がいっぱいあるので、引き続き「これができるようになる仕様がある」みたいな話をしていきます。

Discovery & Dynamic Registraion

f:id:ritou:20210905014914p:plain

まずはOIDCのDIscoveryとDynamic Registrationという仕様の紹介です。

f:id:ritou:20210905014943p:plain

こちらはよくあるソーシャルログインの画面ですが、このボタンってどうやって設定するかわかりますか?一般的なのはサービスが対応するOPを厳選して開発者登録とかして最終的に設定完了したら並ぶみたいな流れでしょう。

f:id:ritou:20210905015001p:plain

Discoveryというのは、RPがエンドユーザーの情報からOPを特定する方法、OPがエンドポイントや仕様のサポート状況を外部に公開する方法が定義されています。

OP特定の方はメアドやURLとかからOPのURLを教えてくれという要求を行うために、 WebFinger と呼ばれる仕組みを利用します。

外部公開の方は、OPは OpenID Configuration と言って特定のパス /.well-known/openid-configuration にて静的なJSON形式のデータを返します。 こちらは GoogleYahoo! JAPAN などいろんなサービスが対応しています。

Dynamic Registrationの方は、動的にRPをOPに登録する方法が定義されています。

f:id:ritou:20210905015048p:plain

GoogleのOIDC関連ドキュメントから拾ってきた図です。JSON形式で各種エンドポイントからサポートする仕様、属性情報とかを提示できます。

f:id:ritou:20210905015106p:plain

Dynamic Registrationのリクエスト/レスポンスの例です。 今日は概要紹介なので詳細は省きます。

f:id:ritou:20210905015123p:plain

これらの仕様のユースケースとして、RP設定の効率化、自動化、半自動化みたいなものがあります。

これを実装しているサービスもいくつか聞いたことがあるのでもしかしたら体験された方もいらっしゃるかもしれません。 OIDCを利用するRPの立場になりうるサービスの設定画面で "Googleと連携する" とかを押すと、エンドポイントの設定がバシッと終わっちゃうみたいなイメージです。

ここでOPがDynamic Registrationに対応していたらRPの登録まで一気に終わっちゃうのでしょうけれども、大体はここは非対応で開発者がOPにredirect_uriや名前などを登録して取得したClientID/ClientSecretの値をRPに登録する場合が多いです。

という感じで、半自動化~自動化もできるようになりますよってところですね。 Discoveryの中でもOpenID Configurationの部分は対応しているOPが多いので、このユースケースは割と一般的になりつつあるのかなってところです。

f:id:ritou:20210905015143p:plain

もう一つのユースケースはさらに実装してるところ見かけないやつですが、 "ユーザーが普段利用しているOP" でログインできるようなものです。 これはOpenID Connectの前のOpenID Authenticationと呼ばれる仕様で採用されていたUX であり、メールアドレスの @以降の部分からOPを特定、今まで使ったことのないOPであれば OpenID Configurationの値を取得してDynamic Registrationするという流れです。

リソースアクセスをターゲットにしているOAuth 2.0の本来のユースケースだとどうしてもベンダーロックインな仕組みに思えますが、OpenIDは本来 "User-Centric Identity"つまり、ユーザーが自分の使いたいOPを選んでID連携機能を利用することを想定した仕組みです。

現状ではDynamic Registrationに対応しているOPがほとんどいないと思うのでこのユースケースはなかなか使われないと思いますが、こういうこともやろうと思えばできるよというお話でした。

Session Management

f:id:ritou:20210905015410p:plain

次はセッション管理に関する仕様を紹介します。

f:id:ritou:20210905020110p:plain

OIDCを用いたID連携において、基本的にはOP/RP間のセッション状態は同期されません。 もちろん、ID連携した瞬間というのはOP/RP共に同じユーザーが利用しているので共にログイン状態となりますが、その後OP/RPのいずれかでログアウトしたらその状態は崩れます。 また、OPと複数のRPが連携している場合、RPごとに同じことが起こり得ます。

f:id:ritou:20210905020131p:plain

そのような状況を改善するために、セッション管理に関する仕様があります。

  • Session Management : この仕様ではOP/RP間でセッション状態が同期しているかを確認する方法を定義しています。具体的には、iframe + postMessageを用いて状態変更がないかどうかを問い合わせます。
  • RP-Initiated Logout : RPからOPにログアウトしたい意思を伝える方法を定義しています。OPの専用エンドポイントにリダイレクトさせることで実現します。
  • Front Channel Logout : OPがフロントエンドのリクエストなどを用いてRPにログアウトする意思を伝える方法
  • Back-Channel Logout : OPがバックチャンネルのリクエストを用いてRPにログアウトする意思を伝える方法

後者2つは昔から "Webビーコン" でやるとか、バックエンドでやるのは確実だけどシステム作るのめんどいなーとか言われてたものを標準化した感じです。

必要に応じて、これらとOIDCを組み合わせて使っていくイメージです。

f:id:ritou:20210905020154p:plain

これらの仕様により、何ができるようになるかというと

  • 複数ドメインで構成される単一サービスをOP/複数RPの関係と捉え、OP上のユーザーのログイン状態に変更がないかをRPは随時問い合わせることができる
  • また、OPのログイン状態に変更があった場合、RP全てをログアウト状態にできる

というあたりです。

前者の例は複数の国に対応するサービスにおいて、トップレベルドメインが異なるURLを複数持つサービスがある場合とかですかね。 とはいえ、最近の3rd Party Cookieに関するブラウザ実装の方針によってはこれらの仕様のうち postMessage や iframe 周りの挙動が変わるかもしれないため、導入を検討する際は注意が必要です。

Identity Assurance

f:id:ritou:20210905020313p:plain

次は Identity Assurance という仕様を紹介します。

f:id:ritou:20210905020335p:plain

なんとかPay、なんとかgramといった決済アプリではもはや馴染みのある "犯収法" "本人確認" といったキーワードですが、この本人確認を行った情報の流通についてのお話です。

自分たちで実装、運用していくとなかなか手間のかかる本人確認 ですが、せっかくならこの確認済みの情報や確認済みフラグといった情報をユーザーの同意のもとで外部サービスに提供しようと思うとどうなるでしょうか?

f:id:ritou:20210905020414p:plain

Identity AssuranceはOPで検証済されたユーザーの属性情報をRPに提供するための仕様です。

"どのルールに従った情報が欲しい" という "要求", "誰がどのルールに従って検証したか" という検証プロせるの情報と値を組み合わせた"表現"、ID連携時のみに提供するかUserInfo Endpointから最新の情報を返すかといった"応答"の仕様が定義されています。

f:id:ritou:20210905020445p:plain

OIDCの属性情報は上で説明しましたが、属性情報の "値" だけでした。 Identity Assuranceでは "誰がどのルールに従って検証したか" という "verification" の情報と属性の値 "claims" によって表現されます。

ちなみにこの "trust_framework" の部分の定義に日本の

も含まれており、"これは犯収法対応のために検証済みの情報です" といった表現が拡張なしで利用できることを意味します。これは国内企業の方が仕様策定に関わることで実現されました。

f:id:ritou:20210905020510p:plain

ユースケースもそのままですね。

  • OIDC : 個別のユーザー認証 -> 認証イベントを流通
  • Identity Assurance : 個別の属性情報検証 -> 検証済み属性情報を流通

という感じで、OPが各種書類やマイナンバーカードで検証した情報の流通はこれから重要になる仕組みのための仕様です。

RISC & CAEP

f:id:ritou:20210905021544p:plain

次は RISC と CAEP という仕様です。

f:id:ritou:20210905021626p:plain

OIDCのフローで説明したID連携処理を行う際、ユーザーは認証処理やアクセス元など含めたリスクベース判定などで安全だと判断されることが一般的です。

しかし、その後例えばネイティブアプリが動作しているスマートフォンやPCのネットワーク環境が変わったりした場合のことを考えると、リソースアクセス時のリスク評価も必要となるでしょう。また、ID連携後にユーザーがOP上でBANや不正利用などでアカウント停止に陥った時、ログインのみに利用しているRPなどはそれを知ることなくサービスを提供し続ける可能性があります。

f:id:ritou:20210905021654p:plain

このような状況を考慮して、OpenIDファウンデーションのWGでは

  • CAEP : 継続的なアクセス評価
  • RISC : アカウントの状態変化を通知

という機能のための仕様策定が進められています。 同時に、前者はmicrosoft、後者はGoogleが実際にプロダクトに導入するなどリファレンス実装も進められています。

f:id:ritou:20210905022349p:plain

RISCの例として後者のRISCユースケースをあげると、ID連携を行ったOPからRPに

  • アカウントの状態変更、退会
  • セッションの無効化

などのイベントをWebhookでJWTを送ることで通知、RPが署名検証して対応する処理を実施のようなことが実現できます。

CIBA

f:id:ritou:20210905021745p:plain

ユースケースを伴う仕様紹介の最後は CIBA です。

f:id:ritou:20210905022430p:plain

最近個人的に注目している、Decoupled Authentication という概念があります。

いわゆる "手元のスマホで認証 + アルファする" っていうお話で、決済の場合は 3D Secure 2.2 からリスク判断結果に応じて手元の端末にPushが送られるフローが導入されています。

このような「手元のスマホでID連携」を実現するための仕組みが CIBA です。

f:id:ritou:20210905022453p:plain

CIBAでどのようにこの Decoupled Authentication を実現するかという点で大事なのが、環境の分離です。

これまで紹介したOIDCのフローではユーザーが操作する User-Agent つまりブラウザが "認証を要求する端末" であり "認証を行う端末" でした。

しかし、CIBAではこれを

  • 認証を要求する端末がユーザーの目の前にある必要はない
  • 認証を行う端末が手元にあり、そこにPush通知などで要求が行く

というように分離します。 認証を行う端末がスマホなどになることで、生体認証やローカル認証を利用するFIDOとの相性も良くなり、より利便性の高いUXの提供にも繋げられるかもしれません。

f:id:ritou:20210905022518p:plain

CIBAのユースケースで重要なのは、事前にユーザーをある程度識別する必要があるところです。 コンビニ決済の例では、客がポイントカードを出す際にそれに紐づくアカウントを店側で保存しておけば「この人に幾らの決済をさせたい」と言う要求ができます。

手元で通知を受け取って決済内容に同意したら、どこぞのQRコード決済のように店員に見せたり端末にスマホをかざして「なんとかぺ〜い」って聞こえるまでの微妙な時間を待つ必要もありません。

f:id:ritou:20210905022547p:plain

CIBAのユースケースとしては先ほどのコンビニに限らずPOS端末全般にNFC端末を置かずに導入できたり、コールセンターや銀行窓口での認証+アルファの確認に使ったりするユースケース、あとはサブスクライブ決済の定期的な確認要求といったところを想定しています。

ここまでのまとめ

f:id:ritou:20210905022612p:plain

前半部分ではID連携に関連する標準化仕様のうち「こんなことができるよ」ってのがはっきりしているものを紹介しました。

ID連携なんて言っても●●でログインだけなんでしょ?と思われている方もいるかと思いますが、実際はその周辺機能についても「この機能も併せて使うよね。なら標準化しよう。」と言った流れでどんどんと拡張されています。

標準化が行われるメリットとしては、ユースケース毎にリスクなどを整理された上で仕様策定が進められることが一番でしょう。 みなさんご存知の通り、ID連携に関わる脆弱性はサービス全体の将来を左右するものになり得ます。 これをやりたい!と思いついたことが既に標準化されていないかを確認し、要件が一致するものは積極的に利用していくことが重要です。

ID連携仕様から学ぶWebアプリケーション間のデータ送受信をセキュアにする方法

f:id:ritou:20210905023446p:plain

後半戦です。

ここからは、ID連携のための標準化仕様に出てくる "Webアプリケーション間のデータ送受信をセキュアにする方法" を紹介します。

標準化種類のタイプ

f:id:ritou:20210905024825p:plain

ID連携のための標準化仕様は

  • 基本仕様 : プロトコルというよりもベースとなるフレームワークや要件を満たす最もシンプルな仕様である程度拡張性を持たせたもの
  • 拡張機能 : 既存仕様と組み合わせたり、一部を置き換えることで安全性を高められる "武装" のような立ち位置にある仕様
  • 特定のユースケースではここに気をつけなれけばならない、と言ったプロファイルやベストカレントプラクティス、あとは脅威と対策の整理したやつとか

というような種類に分けられます。

今回はその中で、拡張仕様でよく使われるテクニックに注目します。

f:id:ritou:20210905024843p:plain

例えば OAuth 2.0 に関連するものは IETF にて仕様策定されてRFCとして発行されているのですが、

f:id:ritou:20210905024904p:plain

ちょちょっと検索して出てくる "RFC 6749 The OAuth 2.0 Authorization Framework" がベースとなるフレームワーク、"RFC 6750 The OAuth 2.0 Authorization Framework: Bearer Token Usage" っていうのが最もシンプルなアクセストークンの種類の定義となります。

それに対してモバイルアプリとかでトークンを安全にやりとりできないケースがあるとなった時にピンポイントで強化する "拡張仕様" が "RFC 7636 Proof Key for Code Exchange by OAuth Public Clients" です。

f:id:ritou:20210905024951p:plain

そして、モバイルアプリやSPAがOAuth 2.0を実装する時に気をつけないといけない細けぇお話みたいなのが BCP としてまとめられています。

その結果、全体で見た時に拡張仕様、とBCPがもりもりになってしまってカオスな状態になってしまい、「新しい話入れずに、一旦まるっと整理しようか」という立ち位置で仕様策定されているのが "OAuth 2.1" と呼ばれているものだったりします。

f:id:ritou:20210905025020p:plain

OpenID Connectの方でもOpenIDファウンデーションにてFAPIというWGが作られています。

f:id:ritou:20210905025528p:plain

元々金融サービス向けのプロファイル作成を目的として作られたのでFAPI("Financial-grade API")という名前になっていますが、ヘルスケア分野のセンシティブなデータのやり取りなどの基準としても使えるようにということで中身は「大事なデータを扱うユースケースにおけるセキュリティプロファイル」と言ったところになっています。

最初は銀行APIの "残高読み取りだけ" = Read Only, "預け入れなども行う" = Read & Write みたいな立ち位置だったのが、"Baseline" と "Advanced" という名前でプロファイルの仕様が分けられています。

f:id:ritou:20210905025702p:plain

ここからは、FAPIでも言及されている仕様で使われている、"Webアプリケーションをセキュアにするための実装のヒント" を紹介します。

と言うと、なんかとても難しい話になりそうですが、実際はそんなことはありません。

直接通信と間接通信

f:id:ritou:20210905025724p:plain

OIDCやOAuth 2.0の仕様を分解していくと、サービス2者間、もしくはユーザーが絡んだ3者間のデータのやり取りです。 それらはアプリケーション間の直接通信、ブラウザが絡んだ間接通信と呼ばれています。

直接通信には "誰のリソース(属性情報)" と言う概念がなくアクセス元が何者かだけを検証するための "クライアント認証" を可能とするリクエストと、"誰のリソース(属性情報)" と言う概念が入った "アクセストークンにより保護された" リクエストがあります。

間接通信では、ブラウザを利用した一般的な GET/POST のリクエストがよく使われています。

直接通信

f:id:ritou:20210905025824p:plain

まず、クライアント認証と呼ばれるところから見ていきます。

"Googleでログイン" みたいな機能を実装する際に、Googleから

  • クライアントID
  • クライアントシークレット

を発行してもらって、Webアプリの環境変数に設定して...みたいな作業を行ったことがあるでしょう。それぞれは識別子とパスワードみたいなものです。

この正しいシークレットを保持していることを確認する仕組みが共有暗号方式、RSAやECDSAなどの秘密鍵/公開鍵のペアを作成して公開鍵を相手に渡して...ってやるのが公開鍵暗号方式と呼ばれていますが、クライアント認証でもこれらに相当したいくつかの方式があります。

f:id:ritou:20210905025852p:plain

最もシンプルなのが、クライアントシークレットをリクエストにそのまま含んで受け取った側が正しいものかどうかを検証する方式です。

  • POSTのリクエストボディのパラメータに含む ("client_secret_post")
  • Authorization ヘッダにBasic認証のパスワード相当として含む ("client_secret_basic")

と言うものですが、プロキシその他なんらかの方法でリクエスト自体を取得されるとクライアントシークレットの値を確認でき、第3者になりすましをされる可能性があると言うリスクもあります。

それを少しセキュアにしようとすると、"そのままじゃなくて署名生成の鍵として使おう" と言う方式です。("client_secret_jwt") リクエストにクライアントシークレット自体が含まれないため、リクエスト自体を取得するだけでは第3者にクライアントシークレットの値を把握されることは困難になります。

f:id:ritou:20210905025917p:plain

さらに公開鍵暗号の仕組みを利用すると、リクエストを作成できるのが秘密鍵の所持者のみ、となります。 これの何が便利かと言うと、何かあった時に「あなた、このリクエストを確かに送りましたよね」と言う証拠として扱えることです。いわゆる否認防止(Non-repudiation)と呼ばれるものであり、センシティブなデータを扱う分野だと重要になってきます。

また、アプリケーションレイヤではなくTLSのレイヤでこれらを実装する方式もあります。Mutual TLSなんて呼ばれていますが、TLSのクライアント証明書を利用するものです。署名生成/検証処理をTLSに任せ、アプリケーションレイヤでは鍵の所持者が適切かと言うあたりを検証するイメージですが、最近はAzureのなんとかやAWSのなんとかで対応が進んでいたりします(説明が雑)ので今後はやってくるかもしれません。

f:id:ritou:20210905025947p:plain

アクセストークンを用いたリソースアクセスの保護については、2種類に分けられます。

  • "Bearer Token" と呼ばれる、アクセストークンを持っているものがその権限を持っていると言う仕組みです。現実世界では電車の切符などがそれに当たります。拾った切符でも有効だったら電車に乗っていけますね。
  • "Sender-Constrained" と呼ばれる、トークンを発行された対象であることを検証可能な仕組みです。海外の航空チケットの場合、(本人と顔写真などで紐づけられた)パスポートとの関連を確認することで搭乗が許可されます。

"Sender-Constrained" の方がセキュアであり、それを実現するための仕組みとしては先ほど紹介したMutual TLSを用いるものや署名付きのJWTを利用するものがあります。

f:id:ritou:20210905030008p:plain

直接通信のポイントをまとめます。 まず、そう簡単に暗号化、Encryptionまでは要求されません。これは割と大事なことです。 クライアントシークレットを利用する仕組みでも、値そのものを送るのではなく署名生成の鍵として利用する方がセキュアである。 そして、トークンを利用する場合も、"Sender-Constrained" にすることでリクエストをセキュアなものにできます。

間接通信

f:id:ritou:20210905030031p:plain

次に間接通信です。 URLをブラウザで開く、でおなじみのGETアクセスですが、OIDCやOAuth 2.0では3種類ぐらいの方式が利用されています。

クエリパラメータを使うのは最もシンプルな方法ですが、ユーザーが手元で値を改竄できたり、サーバーにそのままログが残ってしまうことが懸念としてあります。

OIDCやOAuth 2.0の各種トークンがログに落ちるとそれだけで "漏洩した" となってしまう可能性があるので、GETで重要な値をやりとりする場合はフラグメント部に指定する方式をとります。 "https://example.com/hoge#foo=bar" みたいなやり方ですね。こうするとサーバーに値は落ちないのですが、リダイレクトするときにブラウザがこの値を引き回すかどうか、引き回した時の脆弱性が一時期話題になったりしました。

そして、こちらでも署名付きのJWTにしたら改竄されても検知できるじゃんと言うお話が出てきます。

f:id:ritou:20210905030052p:plain

サーバーにログを残したくないやり方として、POSTでリクエストを送る方法もあります。 個人的には簡単に値をいじってリクエストを送るのがちょっと手間になるのでアレなのと、Microsoftの仕組みはPOSTのリクエストが好みなようです。

こちらも値をそのまま送るのと、署名付きJWTにして送る方式があります。 ちなみにドメインまたぎのPOSTリクエストは受け取った時にHTTP Cookieが送られてこないみたいな3rdParty Cookie, SameSite属性と言ったあたりの話が出てくるので注意が必要です。

f:id:ritou:20210905030146p:plain

OIDCの認証要求を複雑にしたり、認証と同時に決済の要求をしようとしたり...なんてことを考え始めると、どんどん送りたいパラメータが増えていきます。そしてそれをJWTなんかにしたら文字列のサイズが...となることがあるため、直接通信と間接通信の組み合わせもよく使われます。

ここで言うPush/Pullはデータの流れだけを意味していて

  • Push : 先にデータを直接通信で送っておいてそのキーを間接通信に含むやり方
  • Pull : キーとなる値を間接通信で受け取り、受け取った側が直接通信でとりにいくやり方

と言う2種類があります。

なんで両方あるかと言うと、OIDCでの直接通信が

  • RP -> OP 多用されている
  • OP -> RP あまり利用されない

みたいな特徴があるためです。

開発中にlocalhostで立ち上げたRPのWebアプリを考えてみてください。そこからOPに直接通信を送るのは簡単ですが、OPからのリクエストを受け取ることはできません。

一方、間接通信はブラウザがあれば可能です。 最近のゼロトラストの流れで「トラストの境界はネットワークからIDに変わった」なんて言葉を聞くことがあるかもしれませんが、以前はネットワーク制限で直接通信が不可能な場合に間接通信だけで実装可能な仕組みが使われきたんだ、とSAMLに詳しい先輩方に教わりました。OIDCでも同じことができます。

さらにこれをJWTにするのも当然ありです。

f:id:ritou:20210905030220p:plain

あとはちょっと応用として、ID連携においてはRP -> OP -> RPと言う往復、海外では "OAuth Dance" と呼ばれたりしている挙動が出てきます。 2つの間接通信が絡むため、CSRFや値の横取りなどで忙しい世界線です。

f:id:ritou:20210905030249p:plain

このような場合、RP側でセッションと紐づけた値をリクエストに含み、レスポンスを検証する必要が出てきます。 これもJWTと組み合わせることで改竄検知が可能になります。

f:id:ritou:20210905030309p:plain

連続した間接通信について、OIDCやOAuth 2.0ではこれらのパラメータで保護することが可能です。この辺りで検索すると私のブログが引っかかると思うのでよろしくです。

f:id:ritou:20210905030345p:plain

間接通信について紹介しましたが、こちらもそう簡単に暗号化までは求められません。 改竄されたくなかったら署名付きJWTを使う、露出させたくなかったら直接通信で送れと言う感じです。

f:id:ritou:20210905030406p:plain

まとめ

後半のまとめです。 OIDCのセキュリティプロファイルで指摘されている拡張機能で使われている、直接通信と間接通信をセキュアにする仕組みを紹介しました。

f:id:ritou:20210905030429p:plain

ID連携のための標準化仕様のうち、拡張仕様と呼ばれる仕様のいくつかは既存の仕組みのどこかをこの辺りのテクニックを用いて強化するものです。 今回紹介したあたりを意識しておくと、今後もし他の標準化仕様を読む機会ができても怖くなくなるかも...と思います。 もし次回があれば、実際のID連携の細かい処理単位で解説、ってのもしたいなと思ってます。

まとめ

f:id:ritou:20210905030429p:plain

ID連携のための沢山の標準化仕様のうち、今回は「何かをできるようにするための仕様」と「既存の仕様の一部を拡張してセキュアにするための仕様」に着目し、 ゆるふわな感じで 紹介しました。

前半について、ID連携というと「〇〇でログイン」の部分だけ、せいぜいそれとAPIアクセスぐらいを想像される方が多いかと思いますが、関連機能も標準化は進められています。 「あんなこといいな できたらいいな」なんて言う要望を叶えてくれるのはどこぞのポケットではなく標準化活動であると言えるでしょう。

後半について、この分野の仕様を調べていくとどんどん関連する仕様が出てきて頭が混乱しそうになることがあります。(みんなそうです。そしてTwitterで「OAuthなんもわからん」「OIDCなんもわからん」と書いて私の知り合いに本を薦められるのです。) しかし、基本的に通信をセキュアにするための実装パターンは限られており、それをベースとなるシーケンスのどこにどう導入するかというのが拡張仕様そのものになっていたりします。 言ってしまえばID連携なんてのはユーザー情報がメインのサービス間データ連携であり、「枯れた」と表現されることもある定石テクニックが仕様に採用されているわけなので、それを普段のアプリケーション開発に活かすことは難しいことではないでしょう。

以上です。

f:id:ritou:20210905030450p:plain

スライド

speakerdeck.com

超参考になる資料

www.amazon.co.jp

みんなで読みましょう!

ではまた!