ぷらすのブログ

SPA で役立ちそうな OAuth 2.0 for Browser-Based Apps を読んだ

#開発#OAuth#SPA

こんにちは、@p1assです。

GW 中に SPA で OAuth を使うときのプラクティスについて調べていたところ、OAuth2.0 for Browser-Based Apps という RFC の Internet-Draft を見つけました。

一通り読んでみたところ、現時点でのベストプラクティスが良い感じにまとまっていたので、興味深かったところを抜粋して紹介します。

全てを網羅するわけではないので、興味がある方は原文を読んでください。

OAuth2.0 for Browser-Based Apps の概要

OAuth2.0 for Browser-Based Apps は、ブラウザ上で動作するアプリケーションにおける OAuth2.0 のベストプラクティスをまとめた Best Current Practice (BCP) のドラフトです。

すでに存在するネイティブアプリ向けの Best Current Practice である OAuth 2.0 for Native Apps (RFC 8252) のブラウザ版という位置づけになります。

OAuth 2.0 の RFC は昔ながらのサーバーサイドで HTML をレンダリングするアプリケーションを想定しているため、近年の JavaScript を利用した SPA での実装方針はあまり書かれていないです。 SPA に対応した Best Current Practice ができることで、モダンなブラウザベースのアプリケーションでより安全なアプリケーションを作る方法が広く認知されるのでは、と個人的に期待しています。

歴史的背景 : 同一オリジンポリシーと OAuth2.0

OAuth2.0 をブラウザベースのアプリケーションで扱う手段を見る前に、ドラフトに載っていた歴史的背景について紹介します。

ブラウザには同一オリジンポリシーが存在します。 この制限により、ブラウザはクロスオリジンに対して XMLHttpRequest を使ったネットワークアクセスを行えません (一部の例外を除く)。

この制限の中で、ブラウザ上の JavaScript から OAuth 2.0 の Authorization Code Flow を使うのは困難です。 クロスオリジンでホスティングされている認可サーバーに対して JavaScript を通じて Token Endpoint へリクエストを行うことができないからです (D・E の部分)。

     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization Code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization Code ---xxx---'      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token ------xxx----------'
     +---------+       (w/ Optional Refresh Token)

Authorization Code Flow の流れ

Implicit Flow はこの制限を回避するために定義されました。 Implicit Flow では、アクセストークンは URL のフラグメント(#hoge)として渡されます (C の部分)。 フラグメントは location.hash を使って操作できるため、ブラウザ上の JavaScript だけで OAuth2.0 のアクセストークンを使えます。

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+

Implicit Flow の流れ

Implicit Flow のセキュリティ上の懸念

先に述べたように、Implicit Flow を使えばブラウザ上の JavaScript だけで OAuth のアクセストークンが使えます。

しかし、Implicit Flow には特有のセキュリティ上の懸念が伴います。

CORS による同一オリジンポリシーの例外

このような Implicit Flow 特有の攻撃が存在するため、できれば Implicit Flow は使いたくないです。

そこで登場するのがオリジン間リソース共有 (CORS) です。 OAuth2.0 が制定された当時 (2012 年) に比べ、 CORS はほとんどのブラウザで広く使われるようになり、同一オリジンポリシーの例外を作れるようになりました。

CORS を用いることで、ブラウザベースのアプリケーションでも (JavaScript の XMLHttpRequest でも) OAuth 2.0 Authorization Code Flow の Token Endpoint への POST リクエストを実行することが可能になりました。

また、Session History API があるため、ページの再読み込みを行わずに URL のパスとクエリー文字列を変更できます。 つまり、最近のブラウザベースのアプリケーションでは、ページの再読み込みをせずにクエリ文字列から認可コードを削除できます。

これらのブラウザのエコシステムの発展により、OAuth 2.0 Authorization Code Flow をブラウザでも実行できるようになりました。 Authorization Code Flow はブラウザにアクセストークンが漏洩しない (URL に含まれない) ので、Implicit Flow よりも安全です。

良さそう。

アプリケーションアーキテクチャパターン

次に、ブラウザベースのアプリケーションで OAuth 2.0 を使う場合のアーキテクチャパターンを 2 つに分けて紹介します。

バックエンドがない場合 (静的サイトホスティング)

このアーキテクチャでは、JavaScript のコードが静的な Web ホストからブラウザに読み込まれ (A)、その後アプリケーションがブラウザで実行されます。

このアプリケーションは、クライアントシークレットを安全に保管できないため、クライアントは Public Client とみなされます。

                         +---------------+           +--------------+
                         |               |           |              |
                         | Authorization |           |   Resource   |
                         |    Server     |           |    Server    |
                         |               |           |              |
                         +---------------+           +--------------+

                                ^     ^                 ^     +
                                |     |                 |     |
                                |(B)  |(C)              |(D)  |(E)
                                |     |                 |     |
                                |     |                 |     |
                                +     v                 +     v

   +-----------------+         +-------------------------------+
   |                 |   (A)   |                               |
   | Static Web Host | +-----> |           Browser             |
   |                 |         |                               |
   +-----------------+         +-------------------------------+

バックエンドがない場合のアプリケーションパターン

ブラウザの JavaScript アプリケーションは、PKCE (後述) に対応した Authorization Code Flow を開始し (B)、POST リクエストによりアクセストークンを取得します (C)。

その後、適切なブラウザ API を使用して、アクセストークン(と、ある場合はリフレッシュトークン)を可能な限り安全に保存します。

ブラウザの JavaScript アプリケーションはリソースサーバーに対してアクセストークンを含めたリクエストを送り (D)、リソースサーバーからレスポンスを受け取ります (E)。

なお、このシナリオでは、認可サーバーとリソースサーバーは JavaScript を実行しているドメインからの POST リクエストを実行できるように、必要な CORS ヘッダーをサポートしなければなりません。

このアーキテクチャパターンの感想

CORS ヘッダーをサポートするには、認可サーバーとリソースサーバーがクライアントのドメインを何らかの形で登録する必要がありますが、その詳細についてはこのドラフトの中には記載されていませんでした。 もし、1 つの組織が全てを管理するファーストパーティーのアプリケーションであれば、簡単に CORS のヘッダーをサポートできると思います。 一方で認可サーバーやリソースサーバーが Twitter や Google などサードパーティーの場合は一筋縄ではいかないことが予想されます。 (未確認)

あくまでファーストパーティーのアプリケーション内の認可を SSO っぽく行いたいユースケースで使うのが良さそうだと、個人的には感じています。

バックエンドがある場合

React のフロントエンド + Spring Boot のバックエンドのような構成です。

バックエンドにあたる Application Server があるため、JavaScript アプリケーションの外でアクセストークンを取得するためのすべてのステップを実行できます。

そのため、このアプリケーションはクライアントシークレットを安全に保管できる Confidential Client とみなされるべきです [SHOULD]。

   +-------------+  +--------------+ +---------------+
   |             |  |              | |               |
   |Authorization|  |    Token     | |   Resource    |
   |  Endpoint   |  |   Endpoint   | |    Server     |
   |             |  |              | |               |
   +-------------+  +--------------+ +---------------+

          ^                ^                   ^
          |             (D)|                (G)|
          |                v                   v
          |
          |         +--------------------------------+
          |         |                                |
          |         |          Application           |
       (B)|         |            Server              |
          |         |                                |
          |         +--------------------------------+
          |
          |           ^     ^     +          ^    +
          |        (A)|  (C)|  (E)|       (F)|    |(H)
          v           v     +     v          +    v

   +-------------------------------------------------+
   |                                                 |
   |                   Browser                       |
   |                                                 |
   +-------------------------------------------------+

このアーキテクチャパターンでは、まず、Application Server から JavaScript コードが読み込まれます (A)。 このとき、バックエンドに当たる Application Server はリソースサーバーではなく、OAuth のクライアントの一部として考えます。

次に、Application Server はブラウザを認可エンドポイントにリダイレクトすることで、OAuth フローを開始させます (B)。

ユーザがリダイレクトされると、ブラウザは認可コードを Application Server に渡します (C)。 Application Server は Client Secret を使って Token Endpoint で認可コードとアクセストークンを交換します (D)。

Application Server はアクセストークンとリフレッシュトークンを Application Server の内部に保存し、従来のクッキーを介してブラウザベースのアプリケーションと別のセッションを作成します (E)。

ブラウザ上の JavaScript アプリケーションがリソースサーバーにリクエストを行う場合は、代わりに Application Server にリクエストを行い ます (F)。 Application Server はアクセストークンを使って Resource Server にリクエストを行い (G)、そのレスポンスをブラウザに返します (H)。

このアーキテクチャパターンで気をつける点

ブラウザと Application Server のセッションの確立方法

ブラウザと Application Server の間の接続は、Application Server が提供するセッションクッキーであるべきです [SHOULD]。 詳細はこのドラフトの範囲外ですが、HTTP-only や Secure を使うといった多くの推奨事項は OWASP Cheat Sheet シリーズに記載されています。

このアーキテクチャパターンの感想

最近のモダンな Web アプリケーションを作る場合はだいたいこのような構成になると感じています。 フロントエンドとバックエンドが別れているシステムの構成図があるだけでも価値があると思いました。

また、このドラフトでは基本的にトラディショナルなクッキーを用いたセッションを貼ることを推奨していました。 いくつかのインターネットの記事ではクッキーを使わない実装 (JWT を HTTP ヘッダーで受け渡す形の実装など)を推しているものもあるため、人によっては疑問を持つ人がいるかもしれない、と個人的に感じています。

参考 : Proof Key for Code Exchange (PKCE)

Proof Key for Code Exchange (PKCE) は Proof Key for Code Exchange by OAuth Public Clients (RFC 7637)で定義されている、認可コードの乗っ取り攻撃のための対策手法です。

認可コードの乗っ取り攻撃の図 認可コードの乗っ取り攻撃の図 (Authlete さんの記事より引用)

詳細を全部書くと長くなってしまうので、端的に話すと次のような仕様です。

詳しくは、RFC や Authlete さんの記事がおすすめです。

OAuth 2.0 Authorization Code Flow with PKCE を使う際のベストプラクティス

ドラフトでは、アプリケーション側と認可サーバー側それぞれのベストプラクティスが紹介されていました。

ブラウザベースアプリケーション

ブラウザベースアプリケーションをサポートする認可サーバー

感想

CSRF 攻撃に対する対策として PKCE が挙げられていることに加え、"いずれか"という書き方をされているのが印象に残りました。 state を使わずとも PKCE をやっていれば CSRF 攻撃を防げるのはその通りなのですが、state が必須ではない書き方なのが新鮮でした。

おわりに

今回は紹介しませんでしたが、OAuth 2.0 for Browser-Based Apps にはリフレッシュトークンに関するベストプラクティスや認可サーバー側のセキュリティプラクティスなど、まだまだ役に立つものが載っていました。

興味がある方は一読してみることをおすすめします!

← Twitter API v2のOAuth 2.0 Authorization Code Flow with PKCEを試したprotocの代わりにBuf CLIを使ってスキーマ駆動開発の体験を向上させる →
Topへ戻る