2. OAuth 2.0 Security Best Current Practice

このビデオについて

このビデオは、2020 年 1 月 31 日に開催した弊社勉強会プレゼンテーション録画のパート 2 です。 策定が進む OAuth 2.0 Security Best Current Practice に関連する、周辺仕様の概要について、 Authlete の工藤達雄が紹介します。

文字起こし(ログを元に再構成)

OAuth 2.0 Security BCP の概要

工藤: まずひとつ目が、ベストカレントプラクティス、セキュリティBCP です。

もともと "OAuth 2.0" (RFC 6749)、"Bearer Token Usage" (RFC 6750)、"Threat Model" (RFC 6819) という RFC があって、 この中に Security Considerations が、書いてはあるんですね。 ただ 2012 年、もうちょっというとその少し前くらいの使われ方を想定した Security Considerations なので、 ちょっと古くなっている、と。

なのでそれをアップデートしようということで、2016 年の 11 月くらいから、スイスのって言ったらいいのかな、ドイツのって言ったらいいのかな、 Torsten という人が策定を始めた仕様です。 Draft 13 というのが昨年の半ばくらいに出て、ちょっと今日非常にタイミングが悪いことに、 いまこれちょうど expire しちゃって、Draft 13 で止まってるんですけど、 たぶん Draft 14 が、遅かれ早かれ出ると思います。

こういうのが出た背景は、いまちょっとお話しした以外には、使われ方が大分複雑になってきたってところですね。

たとえば金融系の API で OAuth を使ったりとか、医療系でも使ったりとか。

多数との動的な連携というところは、たとえば家計簿アプリみたいな、いろんな API プロバイダーに対してつないでトークンをやりとりするとか。

あと場合によってはダイナミックにクライアント登録をして、クライアント ID とクライアントシークレットを作って登録する、といったようなケースも出てきていて、こういうのはもともとあまり考えられてなかったんですよね。

なので更新しましょう、ということでBCPが出てきてます。 今日これをご紹介するのは、この中にいくつか仕様として新しいのがあって、たぶん、ご存知の部分も あるし、「えっ」っていうのもちょっとあると思うので、いろいろおもしろいと思うんですよね。

BCP の構成としては、40 ページを多いとみるか少ないとみるかってのはあるんですけど、意外とサラッと読める、と思います。 もしかしたら、RFC 6749、6750、6819 を知ってるっていう前提かもしれないですけど、わりと読みやすい。

とくに 3 章に関しては、すごい簡潔にまとまっている。これページ数で言うと 5 ページくらいですよね。この部分だけ読んでも、今の推奨事項というのがわかるので、非常におすすめです。

今日はあまり時間がないので、ここだけお話ししていきます。

CSRF 対策としての PKCE

まず 3.1、Redirect-Based Flows では、 Authorization Code Grant に限らず、ブラウザーリダイレクションを使った場合のフローについて、こうするべきっていうのが書いてあります。

いろいろ書いてあるんですけど、クロスサイトリクエストフォージェリーの対策をしなくてはならないというのが MUST で、 一番上に書いてあるのは state パラメーターを使う、これは昔ながらのやりかたですよね。でもう一個、PKCEでもいいですよ、というのが書いてあると。

なんでここで PKCE が出てくるかっていうのをちょっとお話しすると…PKCE がなんぞやというのを、まずやりますね。

PKCEとはその名前通り、public client で code exchange、 認可コードを使ったフローをどうやって保護するかを念頭に考えられた仕様です。

ちょうど 2014 年とか 2015 年くらいにモバイルでの OAuth 適用がはやってきて、 カスタム URI スキームを上書きされて認可コード(のコールバック)が乗っ取られる、コードのもらい先が違うところになっちゃう、っていう攻撃が出てきたんですよね。

ふつうだと認可リクエストで、正当なアプリケーションが認可リクエストを OS 経由で送って、 そこの redirect_uri には自分自身がインストール時に設定したはずの URL スキームを 入れておくんだけど、あとからこれにぶつけてきた悪いアプリケーションがあると、 OS のほうでですね、認可コードを悪いアプリケーションに戻してしまうと。

ここでの認可コードは、public client なのでクライアント認証無しに、アクセストークンに引き換えられてしまう。 ひいては、いいアプリケーションが作った認可リクエストから、ユーザーの認証に基づいて、悪いアプリケーションに対してトークンが返る、不正アクセスにつながる、というような 感じになって、この対策として出てきた PKCE がある、っていうのがまずは初めなんですね。

(PKCE を適用した場合では、)同じように(悪いアプリケーションがURLスキームを)上書きしていくんですけど、 今度は正当なアプリケーションは、PKCE の中で code_challenge というのをつけた認可リクエストを OS 経由で送る、と。

送られた認可サーバーはこの code_challenge の値を覚えておいて、 そのあとまたユーザー認証・認可したあとにプラウザー、OS経由でまた戻すときには、認可コードだけを戻すんですね。

認可コードを受け取った、この攻撃者のアプリケーションというのは、 そもそも初めに(正当なアプリケーションが)送った code_challenge、正確にいうと code_challenge の元の値 (code_verifier) を知らないので、 このあと code_verifier 無しのトークンリクエストを送ったとしても、 それは、認可サーバーで、本来必要な値がついていないのでダメですよ、ってかたちで弾くことができる。

ひいてはアクセストークンが出ていかない、ということになると。これがもともとの PKCE の役割でした。

なんでこれを使って CSRF の対策になるか。 そもそも OAuth の CSRF とはどういうことなのかをお話しすると、よく OAuth 認証との組み合わせでありがちっていう話なんですよね。

OAuth 認証の定義っていろいろある、まあ、あまりあってほしくはないんですけどいろいろあるとして、 一番わかりやすいのが、アクセストークンをもらったクライアントがユーザー情報を提供する API にそれを投げると、ユーザー情報が返ってきて、その中にたとえば、ひどい場合だとEメールアドレスが入っていて、そのEメールアドレスを使ってクライアントが、あっこの人は誰さんだねっていうのを把握して、8 番で「ようこそ誰さん」っていうのを返す、みたいな感じの、OAuth 認証がある、と。

これを、うまいことすると、攻撃者は途中までこのフローをクライアント経由でやって、その悪い人で認証・認可を行ったあとの認可コードをもらった上で、 ブラウザのほうでまず止めておいて。

ここから CSRF になっていくんですけど、被害者に対して、この認可コードを含む認可レスポンスを送らせる。

たぶん正確にはこの前段でダミーの認可リクエストを <img src=... か何かで送っておいて、もう一回認可レスポンスを埋め込んだのを自動的に踏ませて、クライアントの方でその認可コードと引き換えにトークンをもらって、(認可サーバーが)そのトークンをもらって ID 情報を返す。

ただし、ここ(クライアント)でログインしているユーザーはいい人なんだけど、ここ(トークンレスポンス)で返ってくる ID 情報は悪い人のが返ってくるので、結果的に、クライアント側でのいい人と、認可サーバー側での悪い人がひもづく、ということになると。

これでどうなるか。ふつうにやっていくと、攻撃者はこの認可サーバーで自分自身が知っているID/パスワードでログインをすると、 最終的にはこのクライアントに対して、被害者のアカウントでログインできるようになってしまうことになるわけですね。

この原因としては、途中で人が入れ替わっているわけですね。被害者と攻撃者が入れ替わっている、と。

これを防ぐために、state っていうのを今までは使いましょうと言ってきたわけです。

state をつけることによって、黄色いところ、クライアントが、ここで送った(認可)リクエストに対して、 (認可レスポンスで)戻ってくる state をこの部分でチェックして、はじめに送ったリダイレクトと、 返ってきたリダイレクトに関して、登場人物が同じであるということを把握するというのが、 もともとの state パラメーターの使い方であった、と

PKCE でなぜこれと同じことができるかというと、チェックする場所がちょっと違うんですよね。

全体のつなぎって意味では同じなんですけど、クライアントでチェックするんじゃなくて、今度はサーバー側でチェックしてやる。

同じようにやりとりしたときに、サーバー側で、(クライアントが)はじめにセットした、code_verifier の基本的にはハッシュの値 (code_challenge) をとっておいて、それをもとに、またトークンリクエストが来たときに、送られてきた code_verifier との照合を行なって、 はじめのリダイレクトと、いま送ってきたトークンリクエストというのがひもづくかどうかをチェックして、 一致すればオッケー、ダメだったらどっかで誰かが入れ替わっているということが判別できて、トークンが返らない、ということになります。

なので、PKCE の使われ方が変わってきたというところですね。

認可コード差し替え対策としての PKCE

もう1個、使われ方の変化というのがあって、 もともと Proof Key for Code Exchange by Public Clientsっていうのが、はじめの名前だったんですけども、 去年ぐらい、一昨年くらいからですかね。

認可コードフローを使う場合には public だろうが、confidential、つまりクレデンシャルをちゃんと持っておけるような、(たとえば)Web サーバーのクライアントであったとしても、PKCE を使いましょうというのが、この BCP で書かれています。

これをすると、途中で認可コードの差し替え、あるいは注入、インジェクションが行われた場合にも、それを検知して弾くことができるんですね。

さっきと基本的な流れっていうのは変わってないので説明は割愛しますけれども、これを使うと、 いままでたとえば state とかではできなかった差し替え攻撃に対策ができるようになります。

あと、はじめの CSRF に関しては、state と、PKCE と、あと OpenID Connect の nonce っていうものがあって、どれかがいいですよ、 ていうのが書いてあるんですけど、ちょっと nonce については(説明を)省略します。(値を)チェックする場所がちょっと違うっていうところですね。

Implicit Grant

次のリコメンデーションですけれども、基本的には、implict grant は使うべきではない、と。

もっと言うと、アクセストークンがフロントチャネル、ブラウザを経由するようなケースというのは、このあと出てくるんですけど、 トークンと(それを)送る人のひもづけができないので、使うべきではないっていうふうに書いています。

この(箇所について)、(ドラフト)13 から 14 になるところで、もしかしたら表現がゆるくなる可能性があります。 というのは、この token id_token が、けっこう使われてるんですよね。 フロントチャネルで ID トークンと一緒にトークンを返すっていうケースがけっこう使われていて、これは実際使われているから、使い方によってはいいんじゃない、 ていう意見が出て、表現が弱まる感じです。

ここはとくに(追加の)仕様というのは出てこないです。

Resource Indicators

次に 3.2 を飛ばして 3.3です。

アクセストークンの権限の制限ということで、基本的には scopeパラメーターをまず使いましょうと言ってたのに加えて、 resource パラメーターというのも使いましょうと、出てきています。 もともとこれは(仕様としては)無かったんですよね。実際に動いてるシステムはいくつもありますけれども、(標準)仕様化はされてなかった、と。

Resource Indicators とはなにかというと、認可リクエストのところで、 scope みたいな感じで、アクセストークンを使う先を指定できるようになります。

ここですね。resource っていうパラメーターを認可リクエストに含んで送る、と。 かつ、認可コードが戻ってきてトークンリクエストをするときにも同じように、この resource パラメーターで、 いまからほしいトークンはどこに使うかっていうところを指定できるようになる、と。

これによって、このトークンがあればどのサーバーにも行けるよというんじゃなくて、 特定のサーバー群だとか、あるいは特定のサーバーのみに対して有効なトークンというものを作れる、指定できるようになります。

2 番目(トークンリクエストにおける resource の指定)は、いわゆるダウンスコーピング、はじめに大きな権限のスコープをもらっておいて、徐々に絞っていくような使いかたです。 複数のリソースサーバーに接続可能なトークンをもらう権利を、リフレッシュトークンとしてまず作っておいて、それを使って resource パラメーターで 必要なリソースサーバーに対してのみアクセス可能なトークンをもらう、という流れになります。

ROPC

3.4、次ですけれども、 Resource Owner Password Credentials Grant に関しては、これはもうほんとに一言しかないんですけど、 使わないでね、使うな、という感じですね。

たしか (RFC) 6749 の時代だと まだちょっと移行期間なので、まあしょうがないよね、よっぽどのことがあったら使うしかないね、という感じだったと 思うんですが、基本的には(もう)使わないという感じです。

この背景としては、なんでしょうね、ユーザーの認証の仕組みとして ID / パスワードだけじゃなくなってきたってのがたぶん大きいんだと思います。 名前の通り、これはリソースオーナーの ID / パスワードという文字列をやり取りすることが前提で書かれているので、 現状の二要素認証とか他の認証の仕組みとか、(あるいは関連する)リスクベースを組み合わせるとかとは、ちょっと相性が悪いというところですね。

クライアント認証 (MTLS / private_key_jwt)

3.5 は Client Authentication です。これはまた新しい仕様がいくつか出てきていて、ひとつは MTLS、もうひとつは private_key_jwt。 これらを使って、クライアント認証を公開鍵ベースでやりましょうということが推奨されています。

クライアント認証は、よくあるパターンだと Client ID と Client Secret を、たとえば Authorization ヘッダーに入れるとかボディに入れるだとかして、トークンリクエストのときに送る感じですけど、 そうではなく、そのときに流れる情報というのは公開鍵を使ってやりましょう、ということがここに書いてあります。

それぞれお話しすると、まず MTLS のほうについては、これはまだドラフトで、徐々にマイナーアップデートが繰り返されているようなインターネットドラフトなんですけれども、 トークンリクエストのところで、まず双方向で TLS 接続を持って、ふつうにトークンリクエストを送るんだけれども、認可サーバー側では、あらかじめ設定しておいた Client ID と、クライアント証明書の Subject DN とを比較して、マッチすればトークンレスポンスを返す、ダメだったら弾くっていうような処理をしなさい、ということが書かれてあります。

ふつうにアプリケーション層だけではなくて、TLS も使ってやってください、ということがこの MTLS です。

もうひとつの private_key_jwt のほうは、パラメーターとして公開鍵暗号を組み合わせて、client_assertion を送って、(値の例として) eyJ なんたらかんたらとありますけど、 jwt-bearerassertion_type を同時に送ってくださいね、と いうようなかたちになっています。

どっちがいいかは、たぶん環境によると思うんですけど、次に出てくる Sender Constrained Access Token とからめると、基本的には MTLS 一択かなと思います。

Sender Constrained Access Token (MTLS)

トークンのリプレイ、トークンを勝手に使われるのを防ぐために、Sender Constrained Access Token というのをやりなさい、 ということが出てきていて、ここで選択肢が 2 つあって、ひとつは Token Binding、もうひとつがいまお話しした MTLS です。

ただし Token Binding のほうは、かなり標準化しようとがんばっていたんですけれども、なんというか、 マーケットのサポートがちょっと無くなってしまってですね、実際にサポートしてるベンダーっていうのがたぶんいま無いので、 (今後の実用化が)無くなると思うんですよね。

なのでいま流れとしては、Sender Constrained Access Token をやりたいときの現実的な選択肢としては MTLS しかない、というところになります。 これを使って、トークンを、MTLS でクライアント証明書をちゃんと持っている人だけが使えるようにすると。 とくにリフレッシュトークンに関しては、認可サーバーに対して提示するときに必ずやりなさい、ということが書かれています

どうやるかというと、さっきのトークンリクエストのところまでは同じなんですけど、 このトークンリクエストにおける MTLS、Mutual TLS でクライアントがつないで、まずクライアントの証明書が得られて、 ここ(トークンリクエストのパラメーター)で認可コードとクライアント ID が、こちら(認可サーバー)に送られてくる。 ふつうのトークンリクエストで送られてくるので、それを照合して、そのあと発行するアクセストークンについては、ここで受け取ったクライアント証明書にひもづいたトークンを返しなさい、と書かれてます。

つまり、リソースサーバーも同じように MTLS をしなきゃいけないわけですね。 クライアントはクライアント証明書を使って Mutual TLS を張って、その上でアクセストークン付きの API リクエストを送って、 リソースサーバーはまず通信路のクライアント証明書を取り出して、アクセストークンにひもづくかどうかというのをチェックして、オッケーだったら処理をする、ダメだったらそこでやめる、と。

同じように、認可サーバーに対する、リフレッシュトークンを使ったトークンのリクエストについても、同じように、リフレッシュトークンとクライアント証明書の照合を行なって、OK/NGを判断する、というかたちになります。

こういうのは、めんどくさいっていうとアレなんですけど、けっきょくクライアントのところで証明書を持たないといけない、ひいてはキーペアを作らないといけない、持たないといけないというのがあったので、けっこう、しんどいっちゃしんどいですよね。

とくにこれは public client ではありえないかたちなので、Token Binding が考えられてきてはいたんですけど、ちょっとサポートがなくなった、OS レベルとかブラウザーレベルのサポートがなくなってしまったので、いまはこれ(MTLS)しかない、というところです。

ここまでが、OAuth 2.0 の世界でのセキュリティベストプラクティスで、これはこれでひとつ、ありっちゃありなんですよね、OAuth の中で閉じるんだったら。

もうひとつ出てくるのが、もうちょっと高度なことっていうとアレなんですけど、違うところもちゃんとやりたい、と。 OAuthの2.0のスペックだと、ここまでが、極端な話、限界っちゃ限界なんですよね。 このあとどうするかっていうところで、同じように 2016 年くらいから始まったのが Financial-grade API、FAPI というものです。

3. Financial-grade API に続きます