SPAなClientがトークンを安全に扱えるかもしれない拡張仕様「OAuth2.0 DPoP」とは

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

今日は日頃の情報収集方法の一つである Mike Jones氏のブログ記事に書いてあったドラフト仕様のご紹介です。

self-issued.info

The specification is still an early draft and undergoing active development, but I believe the approach shows a lot of promise and is likely to be adopted by the OAuth working group soon.

まだDraft中のDraftな状態ですが、まいくたんの推し感が出ているのでざっと見ておきましょう。

OAuth2.0 DPoPとは?

tools.ietf.org

This document describes a mechanism for sender-constraining OAuth 2.0 tokens via a proof-of-possession mechanism on the application level. This mechanism allows to detect replay attacks with access and refresh tokens.

Sender Constrained Token については、この辺りから復習しましょう(この前までちょっと内容間違ってたけど修正しました)。

ritou.hatenablog.com

切符はBearer, 国際線の航空券はSender Constrained Tokenみたいな表現のやつです。

f:id:ritou:20190419023842p:plain

OAuth 2.0のAuthorization Code Grantにおいて、

  • Authorization Code, Refresh Token : Sender Constrained Token
  • Access Token : Bearer Token

であるみたいな内容ですが、これはClient Credentialsを安全に管理できるConfidential Clientのお話です。 SPAで動くようなPublic Clientの場合、PCKEを用いてAuthorization CodeをリクエストしたClientに紐付けるぐらいしかできませんでした。(Implicitのことは忘れろ。)

そして、OAuth 2.0におけるproof-of-possession mechanismの2大仕様と言えばこの2つです。

今回のDPoPはこれらを使えないSPAのようなアプリケーションが扱うAccess Token, Refresh Tokenをアプリケーションレベルで "Sender-Constrained Token" にするための仕様です。Confidential ClientもClient認証と組み合わせられるとあります。

細かいところはこれから変わっていくと思いますが、とりあえず重要なところをつまんでいきます。

基本的な流れ

OAuth 2.0 の Authorization Code Grant で Access Token / Refresh Token を発行し、Protected Resource にアクセスするまでの流れを振り返りましょう。

f:id:ritou:20190419001113p:plain

DPoPの対象となるのは、

  • Access Token Request / Response : Client ID, Authorization Code から Access Token / Refresh Token 発行 (Binding)
  • Resource Access w/ Access Token : Access Token を用いたリソースアクセス (Proof)
  • Access Token Refresh Request / Response : Client ID, Refresh Token から Access Token / Refresh Token 発行 (Proof, Binding)

の部分です。Bindingと書いたところで紐付けを行い、Proofと書いたところで紐付けを証明します。

Access Token Request / Response での Binding

Client が Authorization Code を Token Endpoint に送る際、DPoP-Binding ヘッダを指定します。

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
DPoP-Binding: eyJhbGciOiJSU0ExXzUi ...

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
(remainder of JWK omitted for brevity)

この DPoP-Binding ヘッダに指定する値は以下のようなPayloadを含むJWTです。

{
    "typ": "dpop_binding+jwt",
    "alg": "ES512",
}.{
    "jti": "HK2PmfnHKwXP",
    "http_method": "POST",
    "http_uri": "https://server.example.com/token",
    "exp": "...",
    "cnf":{
        "dpop+jwk": {
             "kty" : "EC",
             "kid" : "11",
             "crv" : "P-256",
             "x" : "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
             "y" : "3BttVivg+lSreASjpkttcsz+1rb7btKLv8EX4"
        }
    }
}
  • typ : bindingの時は dpop_binding+jwt, proof
  • alg : none, HS系はダメよ
  • http_method : リクエストのHTTPメソッド
  • http_uri : リクエストのHTTP URI
  • exp : 有効期限
  • cnf : 公開鍵の情報を指定する。bindingの時は必須。

Clientは鍵ペアを生成して秘密鍵を保存しつつ、このようなPayloadに秘密鍵を用いた署名したJWT(JWS)を生成して指定します。 Authorization ServerはこのJWTを検証して公開鍵とAccess Token / Refresh Tokenを紐付けます。

そして、レスポンスでは token_type の値が Bearer+DPoP となります(例は記載されていませんがRFC6749の例をいじるとこんな感じになりそう)。

     HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache

     {
       "access_token":"2YotnFZFEjr1zCsicMWpAA",
       "token_type":"Bearer+DPoP",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
       "example_parameter":"example_value"
     }

Authorization ServerがどうやってAccess Tokenに公開鍵を紐付けるかについては、JWT形式にしてそのまま公開鍵情報を含んでしまうもしくはバックエンドで持っておくとか、よしなに実装します。

Proof

Clientは Bearer+DPoPトークンを利用する際に、DPoP-Proof というヘッダを利用します。

Access Token @ Protected Resources

Access Token を用いて Resource Server にアクセスする時はこうなります。

     GET /resource HTTP/1.1
     Host: server.example.com
     Authorization: Bearer mF_9.B5f-4.1JqM
     DPoP-Proof: eyJhbGciOiJSU0ExXzUi ...

そしてヘッダに指定される値のPayloadはこうなります。

{
    "typ": "dpop_proof +jwt",
    "alg": "ES512",
}.{
    "jti": "HK2PmfnHKwXP",
    "http_method": "GET",
    "http_uri": "https://server.example.com/resource",
    "exp": "..."
}

typ の値に注目ですね。 リクエストを受けたResouce ServerはAccess Tokenに紐付けられた公開鍵でDPoP-Proofヘッダに指定されている値の署名を検証します。 Authorization Server と Resource Server が分かれてる時の公開鍵の扱いとかはよしなにやると。

Refresh Token @ Token Endpoint

Refresh TokenからAccess Tokenを要求するところでも DPoP-Proof ヘッダをつけます。

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
DPoP-Proof: eyJhbGciOiJSU0ExXzUi ...

grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
...

このレスポンスに含まれる Access Token にはリクエストの Refresh Token に紐付けられた公開鍵が同様に紐付けられます。

公開鍵の表現

Access TokenがJWT形式だった時やToken Introspection API でRS / AS 間で公開鍵を表現する時は、cnf というクレームを利用します。

まとめ

とりあえず、今書いてある内容をざっくりと見ていきました。

  • mTLSとかToken Binding使えないClientにSender Constrained Tokenを発行し、AS/RSが検証できるようにするための仕様
  • 新しいHTTPヘッダを利用
  • 公開鍵暗号方式を利用
    • Bindingの時は公開鍵情報と署名
    • Proofの時は署名
  • 攻撃者が一旦偽AS/RSを立ち上げてClientからリクエストを受け取り、それを正しいAS/RSに向けても、検証して気づける

細かいところは今後変わるかもしれません。 鍵ペアや公開鍵の扱いなどはWebAuthnの登録/認証機能のあたりがイメージできる人だとすんなり理解できるかもしれません。

SPA向けと言いつつ、じゃあ秘密鍵をどこに保存すりゃいいのかとかもうちょっと補足がないとClientが安心して使えないかもしれませんが、PKCEとこれの組み合わせで「だいぶ」安全になるなら結構使える仕様なんじゃないかなとか思ったりしています。

今後に期待しましょう。

あれ、何かの告知を忘れている気がする...次回でいっか。

ではまた!