OAuth 2.1 のドラフトから OAuth 2.0 のプラクティスを学ぶ

執筆者 : 山下雅喜


はじめに

OAuth 2.0 が RFC 6749 (The OAuth 2.0 Authorization Framework) としてリリースされてから10年近く経ちました。

OAuth 2.0 はアプリケーションの認可フローが定義されたプロトコルであり、特に Web API を実行するためのトークン発行の用途で用いられています。 また、認証連携のためのプロトコルである OpenID Connect において、IDトークン発行の仕組みとしても OAuth 2.0 が用いられています。 エンタープライズの世界では認証連携のために SAML が使われることも多いですが、オープンなインターネット界隈では OAuth / OpenID Connect が使われていることが多いように見受けられます。

そんな OAuth 2.0 ですが、RFC がリリースされて以降、その仕組みや利用を補助する RFC やインターネットドラフトが複数出てきていました。

これら RFC やインターネットドラフトの内容を受けて、OAuth 2.1 として改訂する動きが2020年から行われており、次のインターネットドラフトが公開されています。 (先日このドラフトの存在を知りました。)

OAuth 2.1 は新たなプロトコルを定義するものではなく、OAuth 2.0 および関連する RFC やドラフトを整理し、OAuth の良いプラクティスを示すものとして位置づけられているようです。

OAuth 2.1 がインターネット標準としていつリリースされるのかわかりませんが、今あるシステムやこれから作るシステムをグッド・プラクティスに沿った形にするために、ドラフトの内容を把握してプラクティスを学んでみます。

OAuth 2.0 と OAuth 2.1 の違い

OAuth 2.0 と OAuth 2.1 の違いは、draft (The OAuth 2.1 Authorization Framework) の Section 10 に列挙されており、6つの違いがあると示されています。

  1. Authorization Code grant フローでは PKCE を標準で使用する
  2. リダイレクトURIの検証は文字列の完全一致で行わなければならない
  3. Implicit grant フローは仕様から除去される
  4. Resource Owner Password Credentials grant フローは仕様から除去される
  5. Bearer トークンをURIのクエリ文字列として渡す方法は仕様から除去される
  6. パブリッククライアントでは、リフレッシュトークンを利用できる送信者または利用回数を制限する

1. Authorization Code grant フローでは PKCE を標準で使用する

スマートフォンのようなエンドユーザの端末上のアプリケーションを OAuth 2.0 のクライアントとする場合、Authorization Code grant フローを用いるでしょう。 しかし、サーバサイドWebアプリケーションと異なり、アプリケーション内に埋め込まれたクライアントシークレットを悪意者はアプリケーションを解析すれば抽出できてしまいます。

また、認可サーバからのリダイレクトレスポンスを受け取ったブラウザがアプリケーションに認可コードを渡す際、OSのカスタムURLスキーム機能を用いることになりますが、エンドユーザの端末上に悪意者のアプリケーションがインストールされていれば、悪意者のアプリケーションは認可コードを横取りできてしまいます。 OAuth 2.0 の仕様ではクライアントシークレットと認可コードがあればトークンを取得できてしまうため、悪意者がトークンを奪えるという認可コード横取り攻撃が成立します。

そこで、RFC 7636 (Proof Key for Code Exchange by OAuth Public Clients) において、認可リクエストを送信したアプリケーションのみがトークンを取得できるようにする PKCE という仕組みが追加されました。 (PKCE の詳細については割愛します。)

PKCE はセキュリティ面を向上させるものであるため、エンドユーザの端末上のアプリケーションに限らず、Authorization Code grant フローを用いる場合には PKCE も標準で使用することとなるようです。

2. リダイレクトURIの検証は文字列の完全一致で行わなければならない

これは、認可サーバにあらかじめ登録されたクライアントのリダイレクトURIと、リクエスト内でクライアントから指定されたリダイレクトURIを、認可サーバがどのように比較するかという話です。

OAuth 2.0 ではリダイレクトURIの比較方法が明記されていないため、RFC 3986 (Uniform Resource Identifier (URI): Generic Syntax) の 6.2. に定義されているように、URIを正規化して同等かどうか比較するという実装も可能となっていました。 しかし、誤った実装により本来リダイレクトすべきでないURIを受け入れてしまうとトークンを悪意者に奪われるという問題を引き起こすため、実装の複雑性を減らすためにリダイレクトURIの比較は文字列による完全一致で行うこととするようです。

3. Implicit grant フローは仕様から除去される

JavaScript ベースの Web アプリケーションで OAuth 2.0 の認可フローを行う場合、クライアントシークレットをエンドユーザに対して秘匿できないため、Implicit grant フローを利用するものとされていました。

ただし、Implicit grant フローにはトークン差し替え攻撃を受ける可能性があり、これを防ぐには、トークンを受け取ったクライアントはそのトークンが自クライアントに対して発行されたものかどうかを認可サーバに問い合わせて確認する必要がありました。 このトークンの確認というステップは OAuth 2.0 の仕様外であり、認可サーバの実装に依存する部分でもあったため、RFC 7662 (OAuth 2.0 Token Introspection) にてトークンを確認するための仕様が後から追加されるといったことが行われていました。

一方、Implicit grant フローを利用せざるを得ない原因であったクライアントシークレットを秘匿できないという課題については、RFC 7636 の PKCE の仕組みによりクライアントシークレットを秘匿できなくても Authorization Code grant フローで安全にトークンを取得できるようになったため、課題が解消されました。 これにより、Implicit grant フローを廃止できる運びとなったわけですね。

4. Resource Owner Password Credentials grant フローは仕様から除去される

Resource Owner Password Credentials grant フローの廃止は当然のことですね。

このフローでは、エンドユーザはクレデンシャルを認可サーバではなくクライアントに渡す必要があるため、悪意のあるクライアントに誤ってクレデンシャルを渡してしまう危険性がありました。 また、2要素認証、WebCrypto、WebAuthn などの認証プロセスを併せて利用することは困難でした。

このフローには OAuth 2.0 より前のシステムからの移行用としての側面もありましたが、認証に求められる要求が高度化し、OAuth / OpenID Connect や SAML のフローが普及した今となっては、Resource Owner Password Credentials grant フローの役割も終えたということでしょう。

5. Bearer トークンをURIのクエリ文字列として渡す方法は仕様から除去される

これは、クライアントからリソースサーバにアクセストークンを渡す部分の話ですね。

RFC 6750 (The OAuth 2.0 Authorization Framework: Bearer Token Usage) にて、アクセストークンをリソースサーバに渡す方法として次の3つが定義されています。

  • Authorization ヘッダを用いる方法
  • Form Body を用いる方法
  • URIのクエリ文字列を用いる方法

これらのうち、クエリ文字列を用いる方法ではブラウザの履歴の中にアクセストークンが残ってしまうため、この方法は使えなくなるということです。 多くの場合 Authorization ヘッダを用いているでしょうから、大した影響は無さそうですね。

6. パブリッククライアントでは、リフレッシュトークンを利用できる送信者または利用回数を制限する

リフレッシュトークンとは、新たなトークンを得るためのトークンであり、OAuth 2.0 の仕様の中でも定義されています。 アクセストークンの有効期限が切れた後でも、認可フローを再度行うことなく新たなトークンを得ることができるため、便利でユーザフレンドリな仕組みです。

逆に言えば、悪意者にリフレッシュトークンが奪われてしまうと、悪意者は新たなトークンを得てリソースを操作できてしまうという問題があります。 リフレッシュトークンの機密性を保護するのはクライアントの責務ですが、リフレッシュトークンが仮に奪われても問題が起きないようにするために OAuth 2.1 で変更しようとしているようです。

変更内容のうち、リフレッシュトークンを利用できる送信者を制限するというのは、RFC 8705 (OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens) のとおり、クライアント証明書による認証を利用して、トークンとクライアント証明書を紐づけようというものです。 高度なセキュリティを要求する Financial-grade API では Mutual TLS の利用が一般的でしょうから、そちらの世界では自然な変更なのかもしれません。

また、変更内容のうち、利用回数を制限するというのは、1つのリフレッシュトークンを1度だけ利用できるようにするという非常にシンプルな変更です。 そもそもリフレッシュトークンが奪われるタイミングとしては、発行されたとき、保管している、利用するとき、のいずれかでしょうか。 利用回数を1度だけに制限することで、利用するときに奪われたとしても、それは利用した後なので問題は起きなくなるということでしょう。

もしも発行されたときに奪えるとしたら、それは同時にアクセストークンも奪えることに他なりませんので、リフレッシュトークンがどうこうという問題ではないですね。 また、保管しているときに奪えるとしたら、アクセストークンやその他重要な情報も奪えるような状況のように思います。

何をしないといけないか

結局、OAuth 2.1 になることで何をしないといけないのか、簡単にまとめてみます。

自身のアプリケーションで OAuth 2.0 が採用された API を利用していたり、OAuth 2.0 による認証連携まがいのことを行っていたりする場合、次のことを行いましょう。

  • Authorization Code grant フローを利用しており、かつ PKCE を利用していないならば、PKCE を利用しましょう。
  • Implicit grant フローを利用しているならば、Authorization Code grant フローおよび PKCE を利用しましょう。
    • もしも API の提供元の認可サーバが PKCE に対応していない場合、PKCE に対応するよう要望を出しましょう。
  • 自アプリケーションのリダイレクト先のURIと、APIの提供元の認可サーバに登録してあるURIが、文字列として完全一致するか確認しましょう。
  • 万が一 Resource Owner Password Credentials grant フローを利用しているならば、認証の仕組み自体を見直して実装し直しましょう。
  • トークンをクエリ文字列に付加してAPIを実行しているならば、Authorization ヘッダとして付加するように変更しましょう。

OAuth 2.0 を採用した認可フローおよびAPIを提供している場合、次のことを行いましょう。

  • PKCE に対応していないならば、PKCE に対応しましょう。
    • JavaScript ベースの Web アプリケーションがクライアントとして存在するなら、認可サーバのトークンエンドポイントの CORS 対応も必要になります。
  • 認可フローの中でクライアントから指定されたリダイレクトURIと、事前に登録されているクライアントのリダイレクトURIを、文字列の完全一致で検証しましょう。
  • リフレッシュトークンの利用に制限をかけていないならば、1つのリフレッシュトークンにつき1度しか利用できないようにしましょう。
    • Mutual TLS に対応するのは敷居が高いかもしれませんので。

こんなところでしょうか。