怖くないネイティブアプリケーションにおけるID連携機能を実装するための考え方

f:id:ritou:20191202101535p:plain

おはようございます、ritou です。

qiita.com

3日目です。やっていきましょう。

ネイティブアプリのID連携

「今年の汚れ今年のうちに」なんていうフレーズがあります。

記憶が確かではないですが、たしか 7 月ぐらいにどこかの決済サービスによりネイティブアプリのID連携に注目が集まったことがありました。

  • バックエンドサーバーがあるネイティブアプリでID連携(ソーシャルログイン)の機能を実装してた
  • ネイティブアプリからバックエンドサーバーに Identity Provider の識別子、ユーザー識別子の組み合わせを送ることで認証状態にしてた
  • 推測したり総当たりなどで...可能性があった

というお話でした。

この記事では "各Identity Providerが提供している SDK などを使ったり使わなかったりしながら安全に認証機能を実現するための方法" を整理します。

Webアプリとの違い

昨日、こんな記事を書きました。

ritou.hatenablog.com

Webアプリではセッションと OAuth/OpenID Connect の各リクエスト/レスポンスを紐付けることで、安全なID連携機能を実装できます。

f:id:ritou:20191118004418p:plain

この考えをネイティブアプリにも適用していくことを考えましょう。

細かいところはおいといて、Clientをネイティブアプリとバックエンドサーバーに分割します。

f:id:ritou:20191203043602p:plain

ここではまず バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という処理の流れを意識することが重要です。

  1. ネイティブアプリとバックエンドサーバー間では独自でセッションが確立している
  2. バックエンドサーバーがID連携のための "チャレンジ" 的な値を生成し、セッションに紐付ける
  3. ネイティブアプリは Identity Provider に "チャレンジ" を含むID連携のリクエストを送る
  4. Identity Provider は "チャレンジ" の値を含むID連携のレスポンスを生成してネイティブアプリに送る
  5. ネイティブアプリはバックエンドサーバーにID連携のレスポンスを送る
  6. バックエンドサーバーは "チャレンジ" の値を含むID連携のレスポンスを検証する

汎用的な説明とするため、あえて "チャレンジ" という表現をしていますが、"チャレンジ" を引回すといえば最近だと WebAuthn の認証フローも構成要素が違うだけで一連の流れは似ています。認証認可のプロトコルでは基本となるやり方だと覚えておくのが良いでしょう。

ここからは、これを実現するための具体的な実装を整理していきます。

大手 Identity Provider が提示する方法

各 Identity Provider がドキュメントで示している実装方法は微妙に異なります。

自分はこれが最強だと思うという実装方法があっても、Identity Provider がサポートしていない機能は使えません。

特に複数の Identity Provider を相手にする必要がある場合はその差異を自分のところで吸収していく必要があるため、気をつけなければなりません。

この辺りの細かいことを考えたくない人は、最初から Auth0 / Firebase とかに魂を売り渡す方が平和に過ごせるでしょう。

それでは一つずつ見ていきましょう。

LINE : Access Token / ID Token

LINE は Access Token, ID Token を用いた連携方法を案内しています。

developers.line.biz

LINE は各種SDKAccess Token / ID Token を検証するエンドポイントを提供しており、ドキュメントを図にするとこんな感じです。

f:id:ritou:20191203092005p:plain

このやり方では、最初に紹介した バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という処理の流れが実現できません。

Access Token を利用する場合、検証APIを叩くことで

  • Access Token の発行を要求した client_id
  • Access Token の有効期限である expires_in

を取得できるため、攻撃者が異なる Client 向けに発行された Access Token , 有効期限の切れている Access Token を指定しても検知可能です。 しかし、ID Token の nonce 相当の値を検証できないため、バックエンドに送られる Access Token が有効な間は同じリクエストで何度もID連携の認証が可能な状態になり得ます。

LINEはAccess Token を無効化するエンドポイントも用意しており、無効化することで Acces Token の検証に失敗するようになるのであれば、認証だけが目的の場合にはログイン成功後にそれを利用するのも一つの手かもしれません。

一方、ID Tokenには nonce の値を持っていますが、ドキュメントにあるやり方では nonce の値を SDK が作成しています。 これでは SDK を用いて連携フローを開始した際のセッションと ID Token が紐付けられていないため、nonce を用いて一度連携に利用した ID Token を再度利用できないように制限するぐらいしかできません。

より安全にするためには、事前にバックエンドサーバーが nonce の値を生成しセッションに紐づけておき、ネイティブアプリは SDK を利用する際にその nonce の値を指定し、最後にバックエンドサーバーが ID Token の nonce の値をセッションに紐付けられているものと比較します。

f:id:ritou:20191203094857p:plain

ちょっと前に調べたときは iOS Swift SDK あたりで nonce の指定ができなかったのですが、新しいバージョンでは外部で生成した nonce を指定できるようになった模様です。

LINE のID連携を利用しているサービスの開発者の方は参考にしていただければと思います。

Google : ID Token

Google のドキュメントではバックエンドサーバーとのやりとりにID Tokenを使えと案内しています。

developers.google.com

developers.google.com

Warning: Do not accept plain user IDs, such as those you can get with the ...

最初にユーザーIDだけでやるなと警告があります。

こちらも説明では一切 nonce に触れられていないため、LINE と同じ話になりそうですが、SDKsetIdTokenNonce という関数があるのでそれを使って バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という流れを実現できそうです。

Yahoo! JAPAN : Authorization Codeフロー

Yahoo! JAPANでは Authorization Code フローを使い、各種トークン取得をバックエンドで行うように案内しています。

developer.yahoo.co.jp

Authorization Code Grant を使う場合、バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という流れを実現できます。

f:id:ritou:20191203101213p:plain

良さそうですね。

まとめ

Binding の仕組みを生かしたネイティブアプリとバックエンドサーバーのID連携の実装について紹介しました。

Sign In with Apple も ID Token あたりでどうにかできそうですし、この辺りを押さえとけば大丈夫かと思います。

これから同じことを考えるときは

  • Identity Provider がどのようなID連携方法を提示しているか
  • SDK に nonce の値をセットできるか
  • Authorization Code Grant が利用できるか
  • ID Token を取得できるか

あたりを調べて、できるかぎり安全なID連携の実装方法を探っていくのが良いと思います。

認証機能でやらかすとサービスが終わる可能性もあります。我々はあのPayの悲劇を繰り返してはいけません。

認証認可技術 Advent Calendar 2019 明日は @authyasan が OAuth 2.0 の Device Flow について書いてくれそうです。楽しみですね。

意識高めすぎて3日やるとか言ってちょっと後悔しましたが穴開けなくて良かったです。

ではまた。

OAuth 2.0/OpenID Connectで使われるBindingの仕組みについて整理する

f:id:ritou:20191202001445p:plain

おはようございます、OAuth警察を装っている ritou です。

qiita.com

認証認可技術 Advent Calendar 2019 2日目の記事です。

今日もやっていきましょう。

今回の内容

OAuth 2.0/OpenID Connect(OIDC)の新しい拡張仕様の記事などを書くときに

  • 「OAuth 2.0のこのフローにはこういう攻撃が考えられる」... ●●●が置き換え可能
  • 「対策としてこれがあるがこのケースでは使えない、もしくは対策がなくて詰んでいた」
  • 「なので考えたのがこの拡張仕様だ」

という流れにすることがあります。というかそもそも新しいユースケースなどに対して未定義の部分や脆弱な部分にパッチを当てるような拡張仕様の場合、Abstract/Introductionの流れがそうなってたりします。 んで、そういうのを見た人がOAuth 2.0やOIDC全体に対して「攻撃や脅威を整理するところから始めて対策を完全に理解しよう!」となったりするんですが、ちゃんとやろうと思うと RFC6819 みたいになってしまい、特に初学者がやるのはボリューム的にもあんまり筋が良くないと思います。

そこで、今回は 個別の事案じゃなく、OAuth 2.0/OIDCを利用する上で最も重要だと思っている "Binding" に注目して各パラメータや拡張仕様を整理します。

ここで言う "Binding" とは OAuth 2.0/OIDC でWebアプリケーションからよく利用されている認可(認証フロー)において

  • ある値をセッションなどと紐付けて
  • どこかでパラメータとして指定して
  • どこかで検証されることで
  • 一連の流れが同一セッションで行われたとみなせる

と言うのを実現するための仕組みです。

この辺りが理解できると

あたりの話も少し楽になるのではないかと思っています。

とはいえ、特に新しい話ではないですし過去に何度も記事を書いてるので、普通に詳しい人はこんなの読んでないで自分のお仕事に集中してください

セッションとの紐付けについて

この後の説明で「セッションと●●を紐づける」という表現を多用しています。 これはレガシーなWebアプリケーションでいうところの

  • HTTP CookieにセッションIDをもち、それに紐づく値をデータストアに格納する
  • HTTP Cookieそのものに紐づく値を署名つきエンコードや暗号化などをして保持する

というような処理のことです。 アドカレ1日目の記事でも取り上げたセッション管理の話ですが、この記事においても本質ではないので細かい実装などには言及しません。

対象となる OAuth 2.0 / OIDC のフロー

この記事では、OAuth 2.0の各種フロー(Grant Type)のうち、最も一般的な Authorization Code Grant と OIDC の ID Tokenを絡めたあたりを扱います。

大まかな流れをシーケンスにするとこうなります。

f:id:ritou:20191118004418p:plain

最初にフロントチャンネルを用いたやりとりがあります。

  • AuthZ(AuthN) Request : Webブラウザなどのリダイレクトを使ってClient/RPからAS/OPにGETのリクエストが送られる
  • AuthZ(AuthN) Response : AS/OPからClient/RPへの戻りの部分で、Webブラウザなどのリダイレクトを使ったGETもしくはPOSTのリクエストで(Authorization Codeなどの)パラメータが送られる

その後にバックチャンネル、いわゆるサーバー間でのやりとりが行われます。

  • Access Token Request : Client/RPからAS/OPへのPOSTリクエストで(Authorization Codeなどの)パラメータが送られる
  • Access Token Response : AS/OPからのJSONレスポンスでAccess Tokenなどが返される

この一連の流れの中で、バックチャンネルのサーバー間通信ではTLS(SSL)の利用やクライアント認証によってリクエストがClient/RPから送られ、AS/OPから返されたことが検証されます。

それに対して、フロントチャンネルのやりとりのところはいくつかのパラメータの受け渡し方が定義されています。

AuthZ(AuthN) Request では JWT を利用することで AS/OP はリクエストが Client/RP により生成された値であることを検証できます。

f:id:ritou:20191202012355p:plain

AuthZ(AuthN) Response でも同様に、レスポンス自体を JWT にしたりハッシュ値を含む JWT を渡すことで、Client/RP はレスポンスが AS/OP により生成された値であることを検証できます。

f:id:ritou:20191202013519j:plain

ここから紹介するパラメータの中には、 AuthZ(AuthN) Response に JWT を利用するものが出て来ますので意識しておくと良いと思います。

state パラメータ by OAuth 2.0

"#state警察" でおなじみ、OAuth 2.0 でCSRF対策を目的として定義されているパラメータです。

  1. Client/RP は セッションとstateパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に state パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた state パラメータをそのまま AuthZ(AuthN) Response に含む
  4. Client/RP は AuthZ(AuthN) Response に含まれた state パラメータがセッションと紐付けられた値と一致することを検証する

f:id:ritou:20191119102956p:plain

HTMLフォームのCSRF対策トークンを Client/RP と AS/OP の間のやりとりで使うイメージで、AuthZ(AuthN) Request と AuthZ(AuthN) Response が同一のセッションに紐づいていることを検証できます。

ここで、AuthZ(AuthN) Responseの検証に注目します。

  • クエリパラメータ/フラグメントとして指定
  • response_mode=post としてPOSTを利用

この場合、state と同時に AuthZ(AuthN) Response に含まれる Authorization Code の値が AS/OP が生成したものであることを検証できません。 悪意のあるユーザーが第3者に向けて発行された Authorization Code の値を取得できた場合、自分の AuthZ(AuthN) Response に指定することが可能です。

それに対して、

  • response_type=code id_token のように JWT 形式の ID Token と一緒に返す : Authorization Code のハッシュ値を含む
  • response_mode=jwt もしくは response_mode=query.jwt のようにJWT形式で返す : state, Authorization Code を Payload に含む

と言う風にJWTを利用する場合は、AuthZ(AuthN) Response 内の Authorization Code が AS/OP により生成されたことを検証できます。

ちなみに FAPI では ID Token に state パラメータのハッシュ値を含む ことも定義されており、その場合は

  • AuthZ(AuthN) Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている
  • Access Token Request : AuthZ(AuthN) Request と紐づいている Authorization Code を指定
  • Access Token Response : AuthZ(AuthN) Request と紐づいている

と全体が安全であることを確認できる状態になります。

state パラメータの特徴としては

  • AuthZ(AuthN) Request が AuthZ(AuthN) Response と紐づいていることを確認できる
  • OIDC の ID Token と FAPI の拡張によって、フロー全体をより安全にできる

となります。

nonce パラメータ by OpenID Connect

"nonce があれば state いらず" なんて言われてたり言われてなかったりする、OIDCで定義されているパラメータです。 こちらはリプレイアタック対策のためのパラメータとして定義されています。

この nonce ですが、OIDC の仕様によりAuthZ(AuthN) Request で scope に openid が指定された場合は Access Token Response に ID Token が含まれ、その Payload に nonce の値が含まれます。

まずは AuthZ(AuthN) Request で response_type に id_token を指定しなかった場合 を見ていきます。

  1. Client/RP は セッションとnonceパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に nonce パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた nonce パラメータを認証イベントに紐づけておき、Access Token Response に含まれる ID Token に nonce の値を含む**
  4. Client/RP は Access Token Response の ID Token に含まれた nonce パラメータがブラウザセッションと紐付けられた値と一致することを検証する**

f:id:ritou:20191129053811p:plain

この場合、AuthZ(AuthN) Request と Access Token Response が同一セッションに紐づいていることが確認できます。

  • Access Token Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている
  • Access Token Request : AuthZ(AuthN) Request と紐づいている Authorization Code を指定
  • AuthZ(AuthN) Response : AuthZ(AuthN) Request と紐づいている(Authorization Code が含まれてた)

というようにフローを逆から見ていくような形で全体の安全性を確認できます。

また、AuthZ(AuthN) Request で response_type に id_token を指定した場合 については

  1. Client/RP は セッションとnonceパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に nonce パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた nonce パラメータを認証イベントに紐づけておき、AuthZ(AuthN) Response に含まれる ID Token に nonce の値を含む**
  4. Client/RP は AuthZ(AuthN) Response の ID Token に含まれた nonce パラメータがセッションと紐付けられた値と一致することを検証する**

f:id:ritou:20191129061101p:plain

となり、全てのレスポンスが AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいていることを確認できます。

  • AuthZ(AuthN) Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている
  • Access Token Request : AuthZ(AuthN) Request と紐づいている Authorization Code を指定
  • Access Token Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている

nonce vs state

ここまでみて、state と nonce ってだいたい同じじゃねーかと思ったりするわけですが、一般的なWebアプリケーションのセキュリティのお話で

  • CSRF対策 : セッションに一意に紐づく値を利用
  • リプレイアタック対策 : 毎回異なる値を利用

という違いがあるため、仕様では state パラメータはセッションに紐づく値、nonce はワンタイムの値を使うように定義されています。

そして、

  • 実装として nonce をセッションに紐づけておいて検証することが一般的
  • nonce についてAS/OPはFAPIのような拡張を入れなくても OIDC の仕様に沿っていれば上記の検証が可能

という面から、"OIDC を調べて nonce の検証したら state の検証いらないって気づいちゃった" 派が一定数出てくるでしょう。

state と同じ、フロントチャンネルで AuthZ(AuthN) Response に含まれる ID Token の nonce を検証するならば state は不要でしょう 。 ただし、フロントチャンネルでは ID Token を扱わず、バックチャンネルでの Access Token Response までやって ID Token の検証をするのであれば、"もっと早い段階でチェックできるものはする(stateのゆるい検証と併用する)" という考え方もありかなと思います。

code_challenge, code_verifier by PKCE

PKCE は "Proof Key for Code Exchange by OAuth Public Clients" です。

  • Secret を安全に管理できない Public Client(ネイティブアプリやブラウザベースアプリなど)は Implicit Grant ではなく AuthZ Code Grant + PKCE 使え!
  • Confidential Client(Webアプリ)も PKCE でよりセキュアに!

という方向性で広く知られるようになりましたが、今回も他のパラメータの説明に揃えてWebアプリケーションでの利用を前提として見ていきます。

  1. Client/RP は ブラウザのセッションとcode_verifierパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に code_verifier のハッシュ値(もしくはそのまま)である code_challenge パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた code_challenge パラメータを認証イベントに紐づけておく
  4. Client/RP は Access Token Request にブラウザのセッションと紐づけておいた code_verifier の値を含む
  5. AS/OP は 認証イベントに紐づけられた code_challenge と Access Token Request に含まれた code_verifier の値を比較(ハッシュ値の検証)し、正しい組み合わせであれば Access Token Response を返す

f:id:ritou:20191129152046p:plain

という流れで、PKCE は AS/OP が AuthZ(AuthN) Request と Access Token Request が同一セッションに紐づいていること を検証します。

全体で見たときには

  • AuthZ(AuthN) Response : Client/RP が確認できるものはない
  • Access Token Request : AuthZ(AuthN) Request と紐づいている code_verifier を指定
  • Access Token Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている

となるでしょう。 AuthZ(AuthN) Response の検証ができないということは、指定された Authorization Code を使って毎回 Access Token Request を送るということになりますが、悪意のあるAuthZ(AuthN) Responseを大量に生成されることにより、Client/RPとAS/OPどちらにも負荷がかかってしまうという事態もありえます。 ここは state や nonce(フロントチャンネルのID Token利用)と併用することで、"早い段階でチェックできるものはする" 設計にする方が良いのではないかと思います。

state, nonce, PKCE を導入しても効果が出ないパターン

ちなみに、Client/RP が state, nonce の検証をサボっても処理を進められますが、PKCEは正しいパラメータを AS/OP に送らなければ OAuth/OIDC の処理は進みません。 この点は AS/OP 側としてはフロー全体のセキュリティのことを考えると安心できる仕組みだと思いますが、気にしなければならないのは、state, nonce, PKCE 共にパラメータを生成するのは Client/RP であることです。 以前、Client/RP のパラメータ生成がよくないと意味がなくなるって話をブログ記事に書きましたのでお時間がありましたらどうぞ。

ritou.hatenablog.com

まとめ

OAuth/OIDCの警察ネタでおなじみ、 state, nonce, PKCE を "セッションとの紐付け" という視点から整理しました。

個人的には Client/RP 開発者は AS/OP がサポートするパラメータはなるべく使って検証もがっつりしとけや と言うところですが、 この記事を読んでそれぞれの処理がどう繋がっていて、途中でパラメータを置き換えられた時にどこで検知できるかを意識してもらえたら幸いです。

だいぶ暖まってきたところで、明日(3日目)の 「認証認可技術 Advent Calendar 2019」は...またまた私です。くどいですね。 今回紹介した "Binding" の仕組みを利用してバックエンドサーバーがいるネイティブアプリの場合にどう認証フローを組み立てていけば良いのかを紹介します。DroidKaigi 2020 に応募して散ったネタであります。

ではまた明日。

Webアプリケーションのセッション管理にJWT導入を検討する際の考え方

f:id:ritou:20191126174956p:plain

おはようございます、ritou です。

qiita.com

これの初日です。

なんの話か

皆さんは今まで、こんな記事を目にしたことがありませんか?

  • Cookie vs JWT
  • 認証に JWT を利用するのってどうなの?
  • JWT をセッション管理に使うべきではない!

リンク貼るのは省略しますが、年に何度か見かける記事です。

個人的にこの話題の原点は最近 IDaaS(Identity as a Service) として注目を集めている Auth0 が Cookie vs Token とか言う比較記事を書いたことだと思っていますが、今探したところ記事は削除されたのか最近の記事にリダイレクトされてるようなのでもうよくわからん。

なのでそれはおいといて、この話題を扱う記事は

  • クライアントでのセッション管理 : HTTP Cookie vs WebStorage(LocalStorage / SessionStorage)
  • サーバーでのセッション管理 : セッションIDを保持 vs 構造体として保持

あたりの話が混在している気がします。

それぞれの細かい指摘はそれなりに正しそうなんですが、全体として「結局どうなん?」となるでしょう。なりませんか?

この記事は比較対象を細かく整理した上で、"JWTを使うために何を考えるべきか" を紹介します。

まずは、Webアプリケーションにおけるセッション管理方法について振り返ります。

Webアプリケーションにおけるセッション管理方法

Webアプリケーションにおけるセッション管理方法で整理すべきはこの2つの比較です。

それぞれを見ていきます。

Cookie vs Token in WebStorage

Webアプリケーションにおいて

  • Single Page Application によるクライアントサイドからのデータ操作が普及してきたぞ
  • OAuth 2.0 のアクセストークンのようなのを用いて自前/外部のAPIにアクセスするぞ
  • で、アクセストークンってどこに保存すればいいんだっけ?

という流れから、

  • JavaScript から読める LocalStorage に保存するとなると、XSSとかが怖い
  • JavaScript から触らせないように HTTP Cookie で HttpOnly 属性をつけたらセキュア?

というお話になります。

その結果、例えば SPA でのAPIアクセスを考えてる人は

  • SPAからのAPIアクセスに利用する期限の短いアクセストークンはリスク受容して WebStorage でもいいかな?
  • それを生成するために必要なやつは Cookie に保存してそこからアクセストークン払い出そうかな?
  • Cookie は常に送られるしサイズとかあるみたいなのででかいのは入れられないかもな... WebStorage に入れといた値と Cookie の値の組み合わせをうまくできないかな?

みたいになるかもしれません。

この部分を検討するにあたり、保存する値が JWT かどうか、それほど関係ないはずです。

まずはこの特性の違いを意識してどこにどのような値を保存するかを考える必要があるでしょう。

セッションIDを保存 vs エンコードされた構造体を保存

JWT が普及する前から、色々な言語の Web Application Framework では、Cookie を用いたセッション管理の実現方法として次のような方法が実装されてきました。

  • CookieにセッションIDを格納、セッションデータはデータストアに格納する
    • データストアには RDBMemcached などのデータストアを利用する
    • セッションIDの破棄だけではなく、データストアで保存されている値を削除することで "セッションの無効化" が可能
  • エンコードされた構造体をセッションCookieに詰め込む
    • 署名の鍵、暗号化の設定ができる
    • 署名などCookieの文字列の有効性だけを検証に利用する場合、"完全なセッションの無効化は困難"。せめて有効期限の概念が必要。

ちなみに、なんとなくCookie=セッションID、WebStorage=構造体を保存するという前提になってる節もありますが、実際はセッションIDを WebStorage 持つこともできますし、Cookieエンコードした構造体を持つこともできます。

そしてこの二つ、どちらかを選択する前提で語られることも多いですが、両者を組み合わせる方式もあるでしょう。

  • セッションIDを含む構造体をCookie/Tokenに詰め込む
    • 署名をつけられる : データストアから引く前に改ざんなどを検知可能
    • 暗号化できる : 構造体の中身を隠せる
    • セッションID以外の値も含められる : 文字列自体に有効期限などを設定できる
    • データストアがある : 無効化可能、フロントエンドに流通させたくないデータはデータストアに閉じ込めておける

両者の特徴を備えるとなるとそれなりに重い実装にはなるでしょう。 扱うデータがセンシティブであるサービスではこのようなセッション管理方式も比較対象に入れても良いでしょう。

ここまでの話で、各WAFや利用されるライブラリに依存している部分があります。

後者の実装まで考えると結果的に JWTの特徴 と大きく関連する内容にはなりますが、まずはどちらの方法を選択するべきか、あるいは組み合わせるべきかを考える必要があるでしょう。

JWTに親でも●されたかのような JWT as a Session な記事の批判は大体がここまで紹介したあたりの話かと思いますが、ここまでの話は JWT が注目される前から存在するものであり、JavaScript での操作の需要が増えて注目されてはいるものの、「枯れた実装」ならぬ「枯れた設計」でしょう。

要件に合わない場合は使う必要はないわけですが、ここからは "それでもなおJWTの導入を検討したい人" が何を考えるべきかというあたりに触れていきます。

JWTの復習

JWT は次の特徴を持つエンコードフォーマットです。

  • 様々なデータをURL Safeにエンコードできる
  • JWS(RFC7515) で署名をつけたり、JWE(RFC7516) で暗号化したりできる
  • 構造化されたデータをやりとりするための標準的な claim が定義されている(RFC7519)
  • 署名や暗号化のアルゴリズム(RFC7518)、鍵の表現(RFC7517)も豊富

これまで紹介したエンコードされた構造体」を標準化した仕様 として導入を検討すると言う意味になるでしょう。

また、(比較対象についてはおいといて)「HTTP Cookieに比べてまだまだ実績が...」みたいな主張も見かけますが、

  • 様々なプロトコルユースケースで利用されている
  • 対応ライブラリも多い
  • JWT生成、検証方法についても一般的になってる
  • 実装における脆弱性や対策も周知されている

といったあたりは「新しくて危ない仕組み」の時期はもはや過ぎているんじゃないかなと思います。 JWT をセッション管理に利用するために考えるべき点を整理します。

何の値を Payload に入れるか

個人的に属性の扱いと言うあたりでは Cookie と JWT を比較しても良いかなと思っていました。

Webブラウザは、Webサーバーが "Set-Cookie" ヘッダにより指定する属性値によってその後の挙動を決定しますが、それだけで完全に Cookie をハンドリングできるかというとそうでもありません。 例えば、Expires, Max-Ageなどで有効期限を指定してもブラウザの開発者ツールなどでユーザーが変更することもできますし、Webサーバーに送られてきたCookieは値だけなので、指定した通りに扱われていることを検証できません。

それに対し、JWT の claim に属性値を指定することで、HTTPのリクエストを受けた時点でそれが改ざんされていないことを検証できます。 RFC7519 で定義されている claim の値を利用することで、複数パーティ間のやりとりだけではなく、Webサーバーが自分自身でハンドリングする場合の検証にも利用できます。

  • iss : 発行者。セッションを発行するサーバー。
  • aud : 受信者。セッション管理の場合は自分自身など。
  • iat, nbf, exp : 発行日時、有効期限周り

Cookie や WebStorage に保存するトークンに誰が何のために発行したものか、有効期限を検証できるようにしたい場合には JWT の Payload にあるこれらの claim を意識し、Signature の検証と合わせて利用することをお勧めします。

それ以外の claim について、JWT の識別について

  • jti : JWT の識別子として「セッションIDを含む構造体をCookieに詰め込む」場合のセッションIDを指定

が使えます。データストアと組み合わせる際にも使えるでしょう。

ユーザー識別子としては

  • sub : ログインセッションの対象となるユーザーIDを指定

が使えます。

識別子の管理ポリシーに従ってPPIDを入れたりJWEを用いて暗号化しても良いでしょう。

JWT の検証パターンと署名アルゴリズム

Cookie や Bearer Token としてなどの HTTPリクエストにJWTの値が含まれる場合、段階的な検証になるかと思います。

  1. 用途(cty あたりで識別)や署名の検証
  2. jti を用いたセッション検証 : セッションDBの参照(※データストアと組み合わせるなら)

署名アルゴリズムについて、monolithなサービスで発行/検証が同一モジュールで行われるような場合は共通鍵暗号方式でも良いと思います。 サービスの規模が比較的大きくなったりしてこれらの検証処理が発行処理と別のところで行われる(署名検証などはProxyやAPI Gatewayみたいな前の方でやり、必要に応じてアプリケーションでデータストアを引くなど)場合、公開鍵暗号方式に変えて検証用の公開鍵を検証箇所に配布するような設計も必要になるかもしれません。

ちょっと前にJWT の署名検証あたりのノウハウについては Qiita に記事を書いたことがあります。

qiita.com

alg がどうこうな話とかは運用で十分に抑えられるでしょう。

終わりに

今回の記事のネタである Cookie vs JWT みたいな比較記事は年に何度かでてくるものの、"JWTだからこそ●●" という部分にはあまり触れらていないものが多いと感じていました。

2011年頃の仕様策定中から JWT をちょっと離れたところから見守ってきた(?)私としては

eyJちゃん、可愛らしイネ(^o^)😃✋ホント可愛すぎだよ〜😄マッタクモウ😃😍🎵💗

(by おじさん文章ジェネレーター)

という感じではありつつも、あくまで "構造化されたデータを安全に送受信するための標準化された仕様" 以上でも以下でもないですし、特徴を活かせそうなところに使っていけば良いと思います。

また、JWT と関連する仕様として、バイナリデータが利用できる環境においては CBOR Web Token (CWT) あたりの活用も少しずつ広まるのかなと感じています。 乱暴にいうと CWT ってのはエンコード方法としてBase64JSONを使っているところをCBORというエンコード方式を利用するものであり、Payload に送受信したいデータを入れるところや署名検証あたりの設計思想は JWT とほとんど一緒です。 今後はJWTを適用しにくいユースケースにおいて構造化されたデータをやりとりしたい時に CWT が検討されていくでしょう。

今回の記事により、有識者に対する「JWTってどうなんです?」という漠然とした質問、JWTの導入検討の悩みを軽減できることをお祈りしています(突然のお祈り)。

明日(2日目)の 「認証認可技術 Advent Calendar 2019」は...また私です。

ではまた。

iddance 2回目やってきました。

おはようございます。

ritouです。

昨日はこれでした。

idance.connpass.com

資料は公開され次第追加予定です。

人の入り具合

前日当日のキャンセルもいくらかあったものの、みなさん来ていただいてありがたかったです。 懇親会参加者は予想+3人ぐらいの健全運営となりました。

内容

今回の発表者は前回の発表者の一部 + 私でした。

idance.connpass.com

個人的には今回も飛ばしていただいて構わん感じだったのですが、実際は「初学者向け」と言う前提を守るべく空気読んでもらった感じもありました。

私は新規登録について取り上げました。

speakerdeck.com

あまりC向けサービスの登録機能を作る機会なんてないと思いますが、 使い始めのUXは重要ですし認証方式の変化に柔軟に対応していくために基本的なところは整理しておく必要があるだろうと言うことで取り上げました。 APIの部分など時間がなくなってしまったので、また今後取り上げたいと思います。

私のに限らず、感想などのblogやTweetをいただけると発表者も励みになると思いますのでよろしくお願いします。

次回

未定ですが、おそらく来年になるかなーと言うところです。

3回めなので話題を散らしたり、もうちょっと個々の発表の後に質問が出るような会になると良いかなと思っております。

ではまた!

(宣伝)

www.sbbit.jp

www.openid.or.jp

webauthn_study という勉強会で話してきました。

おはようございます、ritou です。

昨日はこういう勉強会で話してきました。

web-study.connpass.com

私は WebAuthn をサービスに導入するときに何を考えるべきか、というお題をいただいたのでリカバリーとかリカバリーとかリカバリーの話を中心に細かい仕様よりも認証方式をどう組み合わせるかのような話をしました。

その後ヌーラボの加藤さんのお話を聞けました。

実際に自サービスに導入する前に考えたこと、やったことなどをお話いただきました。 某文章を書くにあたっての疑問をその場で解決しようという流れになり #idcon でおなじみの詳しい人たちが助言をしてくれたおかげで内容も濃いものとなっていたようです。

金曜は #iddance の第2回が行われます。

idance.connpass.com

新規登録はプロビジョニングのないC向けサービスでは重要な機能であり、今後設計を考える機会がありそうなので、現状の整理などをできればと思います。

リカバリーのことを考えないといけないので、ではまた。

builderscon tokyo 2019にて WebAuthn について話しました

お疲れ様です、ritouです。

今年も話してきました。

builderscon.io

資料を作っている時、こんなにスクショとって何がわかるんだ?スクショでUXを語れんの?と思いながら用意していました。 前半は振り返りをしつつWebAuthnのパラメータなど細けぇ話をダイアログのスクショと絡めて説明しました。 後半は各サービスの実装の特徴的な部分を整理し、自分のサービスで取り入れる場合に気をつけることが意識できるようにまとめました。 その結果、合計100ページ超えになりましたが、完全な時間管理を実践して50分ちょうどぐらいに発表が終わりました。これはすごい。これできると思わなかった。きっちり時間を収めたので質疑応答もできました。

Tweetでまとめていただいたかたもいらっしゃって、感謝でございます。(このTweetにどんどん繋がってます)

WebAuthnの実装、結構ちゃんとしてるじゃんと思っていただいた方もいたようなので、これが誰かのお役に立てると幸いでございます。

(おまけ)大入りタオル、割と大きくて良さそう(語彙)

ではまた。

GitHubの2要素認証がWebAuthnに対応したらしいので触ってみた

どーも、ritouです。

世の中みんながこの話題に夢中かと思います(大げさ)。

github.blog

GitHubは以前から2要素認証の方式としてセキュリティキーをサポートしてきました。2015年ですって。

github.blog

今回、その実装がWebAuthnに変更になったということです。

forest.watch.impress.co.jp

将来的には“Windows Hello”や“Touch ID”などもサポートされるという。

Windows HelloやmacOSChromeなどを用いたTouch IDが利用可能になるのは、将来の話ではありません。もう対応しています。

www.publickey1.jp

この記事はパスワードレスを実現したかのような書きっぷり。

GitHubの発表では、パスワードレスとしての利用のことが書かれています。

In addition, WebAuthn can make it possible to support login using your device as a “single-factor” security key with biometric authentication instead of a password. Although we’re not ready to announce further plans, we’ll continue to pursue ways to make secure authentication as easy as possible for everyone on GitHub.

パスワードレス を匂わせていますが、最初にも書いた通り、今回のは2要素認証のセキュリティキーの扱いをWebAuthnにしたっていうお話です。

説明は以上にして、あとは適当にスクリーンショットを見ていってください。

設定

Settings -> Security からやっていきます。

f:id:ritou:20190822165903p:plain

早速セキュリティキーの設定に進むとパスワード確認画面となります。

f:id:ritou:20190822170100p:plain

あれ?GitHubのパスワード確認にセキュリティキーって使えたっけ?今回この辺りも変更になったのかもしれません。 とりあえず進むと、セキュリティキー追加のフローに入ります。

f:id:ritou:20190822171005p:plain

WebAuthnの登録フローを使ってセキュリティキーを登録する場合、ユーザーに名前をつけさせるのが一般的になっています。 今まで見てきた実装では、処理が完了した時点で名前を入れさせるやり方でしたが、GitHubの場合は先に入力させるようです。

f:id:ritou:20190822170255p:plain

名前を入れて続けるとダイアログが。

f:id:ritou:20190822171730p:plain

f:id:ritou:20190822171806p:plain

YubiKeyとかの方と両方試すと

  • Cross-Platform / PlatformAuthenticator 両方に対応
  • UserVerification : 必須じゃない
  • ResidentKey : 使わない
  • Attestation : 要求してこない

というあたりは雰囲気でわかります。

設定が完了したので、ログアウトして認証に利用します。

f:id:ritou:20190822172840p:plain

f:id:ritou:20190822172855p:plain

f:id:ritou:20190822172914p:plain

Androidでも使えそうじゃん。ということで、モバイル用の画面から設定したいところですが、よくわからん。

Chromeから無理やりPC用のURL使って設定します。

f:id:ritou:20190822173047j:plain

f:id:ritou:20190822173118j:plain

f:id:ritou:20190822173132j:plain

f:id:ritou:20190822173152j:plain

で、ログアウトしてFIrefoxでやり直してみましょう。

f:id:ritou:20190822174032j:plain

f:id:ritou:20190822174046j:plain

f:id:ritou:20190822174101j:plain

f:id:ritou:20190822174117j:plain

できました。

GitHubは今回、WebAuthn対応したおかげで、

  • 今までのFIDO2/FIDO U2F対応のセキュリティキー(Cross-Platform Authenticator)だけではなく、Windows Hello / Android / macOS(w/ Chrome)といったOSが提供する機能(Platform Authenticator)を利用できるようになった
  • ブラウザもChromeのみだった(?)ものからWebAuthn対応のブラウザ全体へとサポート範囲が広がった
  • 今後対応環境が増えても自動的に追従可能

といったメリットを享受できます。

今回、セキュリティキーのフローも試しましたが省略します。

これをみた誰かが、手元にある自慢のTitanを使ったフローでも紹介してくだされば良いのではないかと思います。

Titan使った記事がでたー!!!

medium.com

そういえば、最初の方でパスワード確認のフォームが変わったかな?みたいなことを書きました。

今まではパスワード確認しかなかった部分にセキュリティキーを使うとあるので、押してみたところWebAuthnのフローになりました。

f:id:ritou:20190823034401p:plain

f:id:ritou:20190823034414p:plain

これは結構便利だと思います。

来週のBuildersconの準備をしなくてはいけないので、これぐらいにしておきます。 今回のGitHubの例も紹介する予定です。

builderscon.io

ではまた。

Transactional Authorization - "XYZ"と呼ばれる認可プロトコルとは

f:id:ritou:20190730041251p:plain

おはようございます、ritouです。

今回はTransactional AuthorizationとしてIETFにDraftが提出されている仕様に注目します。

draft-richer-transactional-authz-02 - Transactional Authorization

⚠これはOAuth 2.0の特定の脆弱性を防いだりするために作られたOAuth 2.0拡張ではありません。まだドラフトなので今後も変わる可能性があります⚠

資料

少し前にプロトコルの紹介をした時の動画もあります。

www.youtube.com

最近のIETFのお集まりでのプレゼンテーション資料も公開されています。

https://datatracker.ietf.org/meeting/105/materials/slides-105-oauth-sessa-transactional-authorization-xyz

"XYZ"と名付けられたこのプロトコルについての情報は、以下のサイトにて整理されています。

https://oauth.xyz/

英語が聞こえて読めて自分で調べる気持ちがあるならこれで十分でしょうが、理解を深めるために中身を見ていきましょう。

XYZとは?

XYZは、トランザクションモデルをベースとした認可プロトコルです。OAuth 2.0の拡張ではありません。 まずは処理の流れをざっくり説明します。ここで出てくる用語の意味はOAuth 2.0と一緒です。

  1. トランザクション開始要求 : クライアントはバックチャンネルでクライアント自身の情報、対象ユーザーの情報、アクセスするAPIなどのリソース情報、ユーザーインタラクションの情報、鍵情報などを認可サーバーに送信する
  2. ユーザーインタラクション : ユーザーインタラクションが必要な場合、認可サーバーはリダイレクト先のURLなどをクライアントに返し、クライアントはフロントチャンネルでの処理が行われる
  3. アクセストークンの発行 : ユーザーインタラクションが不要だったり完了した後、認可サーバーはクライアントにアクセストークンを発行する

ここでのポイントは2点です。

  • OAuth 2.0で言う所のGrantTypeに関わらず、上記のトランザクション開始からアクセストークン発行までの流れは共通
  • 必要最小限のフロントチャンネルの利用

この後紹介しますが、AuthZ Code GrantとImplicit Grantの場合はAuthZ Requestを送って...Client Credentials GrantやROPC Grantの時は直接Token Endpointへ...というように最初からフローが分けて考えられているわけではなく、必ず最初にトランザクション開始要求が送られます。 ユーザー認証やアクセス許可が必要となる場合だけブラウザでリダイレクトなどをしてフロントチャンネルを利用します。

また、いわゆるAuthZ Request/ResponseにはRFC6749で定義されているパラメータに加え、OIDC/PKCE/JARM/TokenBindingなど拡張仕様をトッピングしていくとパラメータが山盛りになり、戻ってきた後のClient側の検証も必要です。 XYZではトランザクション要求に含まれた情報に紐づくシンプルなURLを認可サーバー側が生成してクライアントはユーザーを送ります。 ちょっと前にOAuth 2.0で決済を行いたいときなどのScopeの指定方法についてのブログ記事などもありましたが、フロントチャンネルのクエリパラメータよりもバックチャンネルのJSONデータの方が柔軟な表現ができるだろうという感じです。

この辺りを意識して、OAuth 2.0の各種フローをXYZで表現したらどうなるかを見ていきましょう。

OAuth 2.0 vs XYZ

今回は次の3つがXYZでどのように表現されるかを見ていきます。

  • Authorization Code Flow
  • Device Flow
  • Client Credentials Flow
  • Resource Owner's Password Credentials / Assertion Flow

シーケンスをいくつか載せますが、その中で登場人物はOAuthとほぼ一緒です。

  • Resource Owner(RO)
  • Resource Client(RC)
  • Authorization Server(AS)
  • Resource Server(RS)

それでは見ていきましょう。

Authorization Code Flow

OAuth 2.0でAuthorization Code Flowは最も有名な認可フローだと思いますが、これをXYZで表現します。

f:id:ritou:20190729030323p:plain

ちょっと小さくて見えないかもしれませんが、

  • Transaction Request : RCはOAuth 2.0のAuthZ Requestに含まれるようなパラメータをTransaction Requestとして送信
  • Interaction Response : ASはRCに interaction_urlhandle(transaction handle) を返す
  • User Interaction : RCはROを interaction_url に送り、ユーザー認証、アクセス許可を行う
  • Callback : RCに戻る際にはCSRF対策用 stateinteract(interact_handle) の値がついてくる
  • Transaction continue request : RCは state の検証後、 handle(transaction handle), interact_handle をASに送信
  • Access Token : ASはAccess Tokenを返す。ここで handle として返される値を用いてRefreshも可能(Transactionは続いていくのだ...)

という感じです。

そんなにシンプルになってる感じではないですが、OAuth 2.0で肥大してしまったAuthZ Requestがバックチャンネルに閉じ込められたようにも見えます。

Device Flow

次はいわゆるDevice Flowです。

tools.ietf.org

もう少しでRFCになりそうですね。こいつをXYZで表現するとこうなります。

f:id:ritou:20190730023848p:plain

  • Transaction Request : RCは interact.type=device な値を含む Transaction Request を送信
  • Interaction Response : ASはRCに user_code_urluser_codehandle(transaction handle) を返す
  • User Interaction : RCはROに別端末で user_code_url にアクセスさせ、 user_code を入力した後にユーザー認証、アクセス許可を行う
  • Polling request : RCは待ってる間、定期的に handle(transaction handle) をASに送信する
  • Access Token : 別端末でのアクセス許可が完了したら、ASはAccess Tokenを返す。ここで handle として返される値を用いてRefreshも可能

OAuth 2.0の拡張ではDevice AuthZ Endpointが追加されたりしてましたが、XYZなら Transaction Request を受ける Transaction Endpoint がその辺もやってくれるので、とりあえず AuthZ Code Flow相当のやつと共存できる感じになっています。 仕様的にはもう一つ "user_code" を含まない場合もありますが、Device Flowの仕様で言うところの "Remote Phishing"、OAuth 1.0で言うところの "Session Fixation Attack" あたりのリスクがあるのでTransaction Requestにはもう少し情報が追加される気がします。

ちなみにTransaction Request/Response の値とASの処理をもう少し拡張すればCIBA相当の処理も実現できますね。

Client Credentials Flow

いわゆる 2-legged なフローですが、XYZでも表現できます。

f:id:ritou:20190730030810p:plain

この場合は Transaction Request にuser, interact フィールドが含まれず、ASはRCの情報を検証してAccess Tokenを返します。

Resource Owner's Password Credentials / Assertion Flow

いわゆるROPCやある外部IdPからのAssertionを受け取ってAccess Tokenを返す Token Exchange フローについても、XYZで表現できます。

f:id:ritou:20190730031139p:plain

この場合は Transaction Request のuser フィールドにクレデンシャルや外部IdPのAssertionなどを含み、ASはそれを検証してAccess Tokenを返します。

こんな感じで、OAuth 2.0でサポートされているユースケースについてはXYZでサポートされており、エンドポイントの構成なども統一したものになりそうなことがお分りいただけたかと思います。

基本的な仕様

ここまではイメージしやすいようにOAuth 2.0との比較しながら見てきましたが、細かいところの理解するために必要な基本的な仕様を整理します。

エンドポイント

  • Transaction Request/Response
  • Transaction continue Request
  • Polling Request

など色々出てきましたが、AS側は "Transaction Endpoint" がバックチャンネルにてリクエストを処理し、User Interaction が必要な場合のみフロントチャンネルを使った処理が行われます。

ハンドル

XYZで扱われる情報をASが処理した時に、それらの情報と紐付けたhandle の値を返すことでその後の処理に利用できます。

  • Transaction Handle : Transaction Request からAccess Tokenを返すまでの一連の処理に紐付く
  • Client Handle : Client情報に紐付く
  • User : ユーザー情報に紐付く
  • Interaction : 個々の User Interaction に紐付く
  • Resource Handle : アクセス対象となるリソースセットに紐付く
  • Key Handle : 鍵情報に紐付く
  • Access Token Handle : アクセストークンに紐付く

これがイメージできると理解は楽になるでしょう。

リクエス

XYZの中で、Transaction Request が一番複雑になり得ます。 XYZのサイト上でここに含むパラメータを色々いじって試すことができます。 盛り盛りにした場合はこちら。

{
    // Client の情報
    "client": {
        "name": "My Client Display Name",
        "uri": "https://example.net/client"
    },

    // User Interaction に関する指定
    "interact": {
        "type": "redirect",
        "callback": "https://client.example.net/return/123455",
        "state": "LKLTI25DK82FX4T4QFZC"
    },

    // User に関する指定
    "user": {
        "assertion": "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJmZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9leGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ.rHQjEmBqn9Jre0OLykYNnspA10Qql2rvx4FsD00jwlB0Sym4NzpgvPKsDjn_wMkHxcp6CilPcoKrWHcipR2iAjzLvDNAReF97zoJqq880ZD1bwY82JDauCXELVR9O6_B0w3K-E7yM2macAAgNCUwtik6SjoSUZRcf-O5lygIyLENx882p6MtmwaL1hd6qn5RZOQ0TLrOYu0532g9Exxcm-ChymrB4xLykpDj3lUivJt63eEGGN6DH5K6o33TcxkIjNrCD4XB1CKKumZvCedgHHF3IAK4dVEDSUoGlH9z4pP_eWYNXvqQOjGs-rDaQzUHl6cQQWNiDpWOl_lxXjQEvQ",
        "type": "oidc_id_token"
    },

    // アクセス対象のリソースについての指定
    "resources": [
        {
            "actions": [
                "read",
                "write",
                "dolphin"
            ],
            "locations": [
                "https://server.example.net/",
                "https://resource.local/other"
            ],
            "data": [
                "metadata"
            ]
        }
    ],

    // Access Tokenなどにバインドする鍵情報について
    "key": {
        "jwks": {
            "keys": [
                {
                    "kty": "RSA",
                    "e": "AQAB",
                    "kid": "xyz-1",
                    "alg": "RS256",
                    "n": "kOB5rR4Jv0GMeLaY6_It_r3ORwdf8ci_JtffXyaSx8xYJCCNaOKNJn_Oz0YhdHbXTeWO5AoyspDWJbN5w_7bdWDxgpD-y6jnD1u9YhBOCWObNPFvpkTM8LC7SdXGRKx2k8Me2r_GssYlyRpqvpBlY5-ejCywKRBfctRcnhTTGNztbbDBUyDSWmFMVCHe5mXT4cL0BwrZC6S-uu-LAx06aKwQOPwYOGOslK8WPm1yGdkaA1uF_FpS6LS63WYPHi_Ap2B7_8Wbw4ttzbMS_doJvuDagW8A1Ip3fXFAHtRAcKw7rdI4_Xln66hJxFekpdfWdiPQddQ6Y1cK2U3obvUg7w"
                }
            ]
        }
    }
}

これがハンドルを使って最もシンプルにするとこんな感じにもなり得ます。

{
    "client": "VBUEOIQA82PBY2ZDJW7Q",
    "interact": "JMMLJ6393FI7ST9B1SRS",
    "user": "XUT2MFM1XBIKJKSDU8QM",
    "resources": [
        "dolphin-metadata"
    ],
    "key": "7C7C4AZ9KHRS6X63AJAO"
}

実装を想像すると処理の分岐など若干めんどくさい気もしなくはないですが、その辺は気にせずに行きましょう。

レスポンス

いくつかレスポンスが定義されています。興味がある方は仕様を見てみてください。

アクセストーク

Transaction Endpointから返されるAccess TokenがBearer トークンの場合はこんな感じで表現されます。

{
    "access_token": {
        "value": "OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0",
        "type": "bearer"
    }
}

Sender-Constrained Token にしたい場合はこんな感じです。

{
    "access_token": {
        "value": "OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0",
        "jwks": {
            "keys": [
                {
                    "kty": "RSA",
                    "e": "AQAB",
                    "kid": "xyz-1",
                    "alg": "RS256",
                    "n": "kOB5rR4Jv0GMeLaY6_It_r3ORwdf8ci_JtffXyaSx8xYJCCNaOKNJn_Oz0YhdHbXTeWO5AoyspDWJbN5w_7bdWDxgpD-y6jnD1u9YhBOCWObNPFvpkTM8LC7SdXGRKx2k8Me2r_GssYlyRpqvpBlY5-ejCywKRBfctRcnhTTGNztbbDBUyDSWmFMVCHe5mXT4cL0BwrZC6S-uu-LAx06aKwQOPwYOGOslK8WPm1yGdkaA1uF_FpS6LS63WYPHi_Ap2B7_8Wbw4ttzbMS_doJvuDagW8A1Ip3fXFAHtRAcKw7rdI4_Xln66hJxFekpdfWdiPQddQ6Y1cK2U3obvUg7w"
                }
            ]
        }
    }
}

Client 認証

OAuth 2.0の場合は静的/動的なClient認証が済んでいる状態を想定しており、動的な登録(Dynamic Registration)を行う場合は処理が1往復追加されます。 XYZではTransaction RequestにClient情報を含むことで動的な登録も処理を増やさずにできますし、静的な登録の仕組みも使えます。 言い換えるとClient情報を細かく制御できる仕組みとも言えそうですが、まぁ、この辺やりすぎると開発者は混乱しそうです。

まとめ

OAuth 2.0のフロートをXYZで表現するとどうなるかを整理し、現状のXYZサイトに書いてある情報を紹介しました。 OAuth 2.0に求められるユースケース毎の要件を整理し、OAuth 1.0の時のようなバックチャンネルのリクエスト中心のプロトコルとして書き直した感じです。

そう言えば決済アプリなどのクレカ関連の処理でお馴染みの3D Secureでも同じようなフローになっています。

3Dセキュア2とは? EMVCoの3Dセキュア

3D Secure 1.0ではXYZのTransaction Request相当のリクエストでカード情報などを送り、ワンタイムパスワードの確認を行うURLにアクセスします。 3D Secure 2.0ではより情報を増やし、リスクベースの判定をしたりします。 Transactionalと言う名前からも、この辺りは共通する部分だったり参考にできる部分はありそうですね。

今後どのような展開を見せるのかはわかりませんが、引き続きウォッチしていきたいと思います。

告知

idcon は今週末開催です。

idcon.connpass.com

ここに来る人なら今回の XYZ についてもすぐに理解できるよな! 興味があったら懇親会でお話ししましょう。

ではまた🍎

GitHub で使われている Facebook の Delegated Account Recovery とは(概要編)

おはようございます、ritou です。

様々なご都合によりGitHubでTwo-factor authenticationってのを設定している方も多いでしょう。

時に人間は、記憶もスマホも財布も一気に無くしてしまいます。

リカバリー方法を複数用意しておくにこしたことはありません。

2019年7月時点の設定画面あたりのスクショはこんな感じです。

f:id:ritou:20190712022802p:plain

今回はこの中のRecovery optionsの一番下、「Recovery tokens」に注目します。

これ何だ?と思ってカーソルを当てると「Account recovery with Facebook is a simple way to recover your account.」とか出てきます。

f:id:ritou:20190712023201p:plain

今回はこの「Facebookでアカウントリカバリー」とは何かというお話です。

GitHub の機能を使ってみる

GitHubのヘルプに全部書いてあります。

これで満足していただけるようであればそれで良いと思いますが、一応書いてある通りやってみます。

リカバリーの設定

GitHubにて「Settings」->「Security」->「Two-factor authentication」->「Recovery tokens」と進みます。

f:id:ritou:20190712023945p:plain

この機能では 「お前のデータにアクセスはしないけど、こっちのサポートチームがお前のIdentityを検証するために使える。」 「まずはfacebook行って来い」 てなことが書いてあります。

Facebookに行ってみると何かの確認画面が出てきますが、いつものアクセス許可への同意画面とは何か少し違います。

f:id:ritou:20190712024354p:plain

「詳しくはこちら」の先も確認しましょう。

f:id:ritou:20190712024501p:plain

「いかがでしたでしょうか?」でまとめてくるブログ記事みたいな雰囲気のタイトルがついたヘルプページですが、基本的にデータはシェアされないことが書いてあります。

オンにすると設定完了です。

f:id:ritou:20190712024624p:plain

Githubに戻ってきました。

リカバリーの実行

GitHubを一旦ログアウトして、Facebookからリカバリーする手順を踏んでみましょう。

今度はFacebook側で「設定」->「セキュリティとログイン」->「外部アカウントのリカバリー」と進みます。

GitHubの設定があることがわかります。

f:id:ritou:20190715041734p:plain

リカバリーしてみます

f:id:ritou:20190715041823p:plain

(ここは人によりそうですが、私は2段階認証でSMSを設定しているせいか、確認が入りました。)

f:id:ritou:20190715042055p:plain

GitHubに遷移して...こんなメッセージが出てきました。

f:id:ritou:20190716024805p:plain

一発でログインさせるのではなく、サポートチームに「Facebookリカバリトークン使った復活をお願いします」と問い合わせてやってもらう流れです。

(おまけ)ちなみにもう一回試そうと思ったらこんなんなりました。

f:id:ritou:20190715042154p:plain

  終
制作・著作
━━━━━
ⓡⓘⓣⓞⓤ

この仕組み、標準化されたものではなくFacebookの独自のものです。Facebook側のドキュメントを見て仕組みを理解しましょう。

Facebookの仕様を理解する

ここですね。

developers.facebook.com

クローズドβということですが、GitHub以外に使ってるとこあるんでしょうか?なさそう?

概要

概要としては

  • パスワードや連絡先を失った場合のリカバリーに利用可能
  • OAuth/OIDCを用いたソーシャルログインとは異なり、ユーザー情報を共有しないシンプルな仕組み
  • メールやSMSへのコード送信よりもセキュア、連絡先変更時のトラブル回避にも使える

とあります。(私の場合、SMSの確認が入ったので若干気になるところもありますがこの辺は設定次第かもしれません。) 用途についても書いてますがとりあえずリカバリーです。

処理の流れ

登場人物は3者です。

  • Account Provider : Delegated Account Recoveryを利用するサービス (GitHub)
  • Recovery Provider : Delegated Account Recoveryを提供するサービス (Facebook)
  • User : GitHub / Facebookの両方にアカウントを持つユーザー

設定手順としては

  1. UserがAccountProviderにて認証済み or 新規登録中
  2. UserはAccountProviderにてサポートされているRecoveryProviderを選択
  3. AccountProviderはRecoveryTokenを生成、Userのブラウザ上でRecoveryProviderに送信 4 RecoveryProviderはUserにRecoveryTokenを紐付けて保存し、AccountProviderに戻る

という4ステップがあり、実行フローも

  1. Userはリカバリーが必要なことをAccountProviderに伝え、AccountProviderは紐づいているRecoveryProviderにUserをリダイレクト
  2. UserはRecoveryProviderにて認証される(リカバリ用途だとわかっているので、追加認証を求められる場合もある)
  3. RecoveryProviderはAccountProviderから受け取って保存していたRecoveryTokenを含むCountersignedTokenを生成し、Userのブラウザ経由でAccountProviderに送る
  4. AccountProviderはCountersignedTokenがRecoveryProviderからのものであることを検証、RecoveryTokenを検証したら内部のデータを複合化してUserをリカバリーします

という4ステップです。 シーケンスについては上記ページに記載してあります。

技術的なポイントとしては

  • 準備
    • OAuth/OIDCのClient登録的なものは必要なのか
  • 設定
    • AccountProvider(GitHub)が作るRecoveryTokenとはどんなものか
    • AccountProvider(GitHub)はRecoveryTokenをどのようにRecoveryProvider(Facebook)に送るのか
    • RecoveryProvider(Facebook)はRecoveryTokenをどのように扱うのか
  • 実行
    • RecoveryProvider(Facebook)が作るCountersignedTokenとはどんなものか
    • RecoveryProvider(Facebook)はどのようにしてCountersignedTokenをAccountProvider(GitHub)に送るのか
    • AccountProvider(GitHub)はCountersignedTokenをどのように検証するのか
  • 妄想
    • OIDCをシンプルにして同じことできないか

といったあたりが気になりますね。 書いてたらとても長くなって諦めたので、次回プロトコル編として公開したいと思います。

ではまた!

OAuth 2.0 / OpenID Connectにおけるstate, nonce, PKCEの限界を意識する

f:id:ritou:20190708035757p:plain

おはようございます、ritouです。ちなみに予約投稿なのでまだ寝てます。

本日のテーマはこちらです。

OAuth 2.0で言うところのClientの視点から、ここに気をつけて実装しましょうという話ではありません。

OAuth 2.0で言うところのServerの視点からみて、Clientにこんな実装されたらたまんねぇなっていうお話です。

最終的には一緒な気もしますが、とりあえず始めます。

state

OAuth DanceにおけるCSRF対策としての state パラメータについて簡単に整理します。

  1. Clientがセッションに一意に紐づく値として生成、管理
  2. ClientがAuthorization Requestに付与
  3. ServerはAuthorization Requestとして受け取った値をAuthorization Responseにそのまま付与
  4. ClientはAuthorization Responseとして受け取った値を検証

しかし、Clientに次のような実装をされると、state の意味が無くなります。

  • セッション問わず常に同じ値を指定する
  • 実は検証してない

これらをServer側で防ごうとしても、限界があります。

  • state の検証まで行うSDKを提供しても使われないかもしれない
  • AuthZ Requestに state の付与を必須にしても、ワンタイムにしろと定義されているわけでもないためキャッシュして弾くような実装にもできない
  • 検証しているかどうか、Webアプリケーションからの利用などはプラットフォームから配信されるネイティブアプリのように審査で完全にチェックできるわけでもない

Client側の実装が正しく行われなくても処理が完結できる作りになっている以上、Serverとしては手が出せません。

nonce

OIDCの nonce パラメータ、ご存知でしょうか?なんか聞いたことある? なんとかPayの前にちょっとだけ話題になってた「Sign-In with Apple」の実装とOpenID Connectの仕様の差異についての記事を目にされた方もいらっしゃるでしょう。

japanese.engadget.com

nonce 扱いが、この件の仕様の差異として出てくるのですが、一旦整理します。

  1. ClientがAuthZ Requestを送る際に生成、管理
  2. ClientがAuthorization Requestに付与
  3. ServerはAuthorization Requestとして受け取った値をAuthorization Response/Access Token Responseに含まれるID Tokenに含む
  4. ClientはAuthorization Response/Access Token Responseに含まれるID Tokenを検証

ID Tokenに含まれているので、Clientがネイティブアプリ-バックエンドサーバーと言う構成になっていたりしても値を引き継いで検証可能です。

当然こちらも、Clientに次のような実装をされると意味が無くなります。

  • 同じ値が指定される
  • 実は検証してない

これらをServer側で防ごうとしても、state と同様に

  • nonce の検証まで行うSDKを提供しても使われないかもしれない
  • AuthZ Requestにnonceパラメータの付与を必須にして値のハッシュ値とかをキャッシュして弾いてやろうと思っても色々悩む
  • 検証しているかどうか(以下略

となります。

個人的に、キャッシュするとかどうとかのあたりはライブラリで実装しても良いかなと思った時期がありましたが、

  • フォーマット自由だしな...ハッシュで保持するしかないか?
  • いつまで保持するの?データ量...たくさんのClientからリクエストが来るServerだったら...

など、得られる効果に対してなかなか悩みどころが多そうな感じです。 と、これを書きながら、ふと思い出しました。 10年以上前の2007年だか2008年に策定されたOpenID Authentication 2.0っていう仕様では、ここで言うAuthZ ResponseにServer側がnonceを払い出す、かつフォーマットの指定まで定義されていました。Final: OpenID Authentication 2.0 - 最終版

openid.response_nonce 値:長さが 255 文字以下の文字列で、この成功した特定の認証応答に固有のものでなければならない (MUST)。ノンスは、サーバの現在時刻で始まらなければならない (MUST)。またノンスには、それぞれの応答を固有のものとする上での必要に応じて、33-126 の範囲に含まれる ASCII 文字 (空白を除く印刷可能な文字) を追加してもよい (MAY)。

その上で、Client側でもちゃんと検証しろと書かれていました。

"openid.response_nonce" について、当該 OP から、これまでに同じ値のアサーションを受け入れたことがない

今回のnonceパラメータもこれぐらい厳密であれば、Server側で管理できなくもない気もしていますが、現状はやはりClient側の実装が正しく行われなくても処理が完結できる作りであることを受け入れざるを得ません。

ちなみにOAuth 2.0/OIDCよくわかってないけどWebAuthnわかる方は、WebAuthnのフローに出てくる challenge パラメータをイメージしていただけると良さそうです。

WebAuthnでもClient/Authenticatorは手元のブラウザ、セキュリティキーですし、challengeの値をキャッシュして重複を弾く実装を確実に行うのは難しそうな印象です(詳しい人の意見求む)。

PKCE

最近のOAuth/OIDCの議論や実装でよく見かけるようになったPKCEについても整理します。

  1. ClientがAuthZ Requestを送る際に code_verifier, code_challenge, code_challenge_method を 生成、管理
  2. ClientがAuthorization Requestに code_challenge, code_challenge_method を付与
  3. ServerはAuthorization Requestとして受け取った値をAuthorization Codeに紐付けておく
  4. ClientはAccess Token Requestに code_verifier を付与
  5. ServerはAuthorization Codeとcode_veirifier を検証

d.hatena.ne.jp

state, nonceと一緒なのは、Clientが生成するってところです。 state, nonceと異なるのは、Serverが検証するし、その検証をしないと処理が完結しないところです。

と言うことで、Clientに次のような実装をされると意味が無くなる点としては。

  • 同じ値が指定される

ぐらいでしょうか。

  • パラメータ生成機能を持ったSDKを提供しても使われないかもしれない
  • AuthZ Requestへのパラメータの付与を必須にして値のハッシュ値とかをキャッシュして弾いてやろうと思っても(略

と言うあたりは残ります。

まとめ

ここまでを一旦まとめると

  • Clientが生成するものをServerは完全には検証できない : 細かなフォーマットがあればなんとかなるかもしれないが...
  • Clientが検証しなくても処理が完結してしまうものに対してServerは手を出せない

と言う当たり前のお話でした。

今回、なぜこれを書いたかと言うと世界平和のためにTwitterしてたら急に思い出しました。

もう6年前の話なので忘れかけていますが、某プラットフォームでOAuth 2.0(RFC6749/6750)に続いてOpenID Connectの仕様の最低限のところを実装し、これで「〜でログイン」させたいというのを内部の然るべきルートで相談したところ、当時大先輩と呼んでいたセキュリティな人に上述の state の話をされました。 セキュリティレビュー、ちゃんと機能してた!(大事)

ID連携をグループ内だけで展開するので Client のチェックができる環境、SDK利用を強制できるなどの場合はまた別だったかもしれません。 しかし、一般開発者向けに提供される仕組みにおいて、全てのClientの実装を継続的にチェックする/できるような状況にならない限りは許容できない→確かにそうですねという感じで、対策として独自の拡張機能を実装することになりました。

そして独自拡張というのは、PKCE で言う所の code_challenge_method=plaincode_verifier(=code_challenge) を Authorization Server が発行してワンタイムになるように管理するものです。

alpha.mixi.co.jp

  • Client が ServerState を要求し、 Authorization Server が Client と紐付けて発行
  • Client は AuthZ Requestに含み、Authorization Serverは ServerState の有効性、紐付けられている Client を検証
  • Authorization Server は Authorization Code と紐付け、AuthZ Response には含まれない
  • ClientはAccess Token Request に Authorization CodeとServer Stateを含む
  • Authorization Server は それらを検証

code_challenge_method=s256 のようにしようと思ったら、それもできます。 今思うと新しいEndpointを増やしたくなかったのでToken Endpointから返してますが、この用途なら新しいエンドポイントで返すべきだと思っています。 そして、ここから先(標準化に向けた取り組み)をせずに野良のまま放置してしまったのも私の怠慢です。

ちなみに相談の時に nonce の話をしたかどうかは忘れましたが、PKCEが出た後だとしても同様の展開になったと思います。 と、言うことで、最後にもう一度まとめます。

  • state, nonce, PKCEがうまく機能しなくなるClient側の実装を意識しよう
  • 完全ではないということでこれらの仕組みがダメだという話ではなく、リスクをどの程度まで軽減させ、受容できるのかを考えるのが大事でしょう
  • 自分の場合はセキュリティ担当からの指摘によってServerStateと言う独自拡張が爆誕した過去の思い出

以上です。今週も頑張りましょう。

ではまた。