図解 DPoP (OAuth アクセストークンのセキュリティ向上策の一つ)

# はじめに

**DPoP**（ディーポップ）という仕様を紹介します。

|OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)|
|:--|
|https://datatracker.ietf.org/doc/draft-ietf-oauth-dpop/|

この仕様は、悪い人が**アクセストークンを盗んだとしても、それだけでは API に対する不正アクセスができない**ようにするための仕組みです。

従来は、クライアントアプリケーションがアクセストークンを提示して API にアクセスしてきたとき、そのアクセストークンが有効であれば、API アクセスは許可されていました。しかし、DPoP などの **Proof of Possession**（**PoP**）の仕組みを用いると、アクセストークンを提示しているクライアントがそのアクセストークンの正当な所有者かどうか（＝アクセストークンの発行を受けたクライアントと同一かどうか）もチェックされるようになり、**アクセストークンの正当な所有者とみなされない場合は API アクセスが拒否される**ようになります。

PoP の方法として [RFC 8705](https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens") の [Section 3](https://www.rfc-editor.org/rfc/rfc8705.html#name-mutual-tls-client-certifica "Mutual-TLS Client Certificate-Bound Access Tokens") で定義されている方法（通称 **MTLS**）が使えるのであればそちらのほうがよいですが、それができない場合、例えば Web ブラウザ内で動くシングルページアプリケーション（SPA）の場合、DPoP が PoP の候補となります。

以下、DPoP を図と文章を用いて説明していきますが、動画のほうがお好みであれば、こちら（↓）をご覧ください。

|OAuth & OIDC 勉強会 アクセストークン編 by #authlete - 5. Proof of Possession (6:55〜)|
|:--|
|https://www.youtube.com/watch?v=ampSEBW6vKM&feature=youtu.be&list=PLxDcFnLrbxvafUBu2GtX35G-iiktmZhWa&t=415|

それでは説明を始めます。


# 1. 図解 DPoP

クライアント、認可サーバー、リソースサーバーがあります。これらの関係については『[一番分かりやすい OAuth の説明](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)』を参照してください。
![dpop_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/a7875094-c395-baf1-8662-a5717dacf615.png)

**（１）**クライアント側で公開鍵と秘密鍵のペアを作成します。
![dpop_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/6689bd9a-be4e-06c5-43b2-038a390fd64d.png)

**（２）**作成した公開鍵を含む JSON（後ほど生成する JWT のヘッダーとして利用）と、のちほど投げるトークンリクエストに対応するペイロード（ただしトークンリクエストのペイロードそのものではない）を用意します。
![dpop_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/242a51b3-4b0d-7e94-5480-8343727310a3.png)

**（３）**（２）で用意したデータを入力とし、秘密鍵を用いて署名を生成します。
![dpop_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/6fc7adbf-1b6a-37e1-b3a0-ec6ec51d1498.png)

**（４）**これらを一つにして JWT とします。この JWT を『**DPoP proof JWT**』と呼びます。
![dpop_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/df4ba963-6bea-5854-a94c-b5b2c013dd2c.png)

**（５）**トークンリクエストをおこないます。この時、先ほど生成した DPoP proof JWT を含めます。
![dpop_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/cd12f95b-2797-2910-62b3-30b5baaeddc0.png)

**（６）**トークンエンドポイントの実装は、リクエストから DPoP proof JWT を取り出します。
![dpop_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/13930e71-e622-3e27-8c67-4a255c407674.png)

**（７）**DPoP proof JWT に含まれている公開鍵を用いて署名を検証します。この検証により、**クライアントが当該公開鍵に対応する秘密鍵を持っている**ことを確認できます。
![dpop_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/55ec7074-9030-72d5-27b5-40a1900dd321.png)

**（８）**アクセストークンを生成します。
![dpop_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/148bd549-5075-4dc1-48c3-61acae7dd44b.png)

**（９）**生成したアクセストークンと公開鍵を紐付け、その関係を覚えておきます。紐付け方法の詳細は後ほど説明します。
![dpop_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/4f7ce489-5c40-1122-d2c4-40f57a40db0d.png)

**（１０）**アクセストークンを発行します。
![dpop_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/3dfd3cf2-d9df-df1c-5852-6f7100966887.png)

**（１１）**クライアントはアクセストークンを用いて API コールをおこないますが、その前に一手間かかります。（１）で作成した公開鍵を含む JSON と、API コールに対応するペイロード（ただし API コールのペイロードそのものではない）を用意します。
![dpop_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/3a3126f1-7e5c-2161-d63b-b660faf38aa4.png)

**（１２）**（１１）で用意したデータを入力とし、秘密鍵を用いて署名を生成します。
![dpop_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/68980391-34e4-63a9-e3bf-5c7c49d97602.png)

**（１３）**これらを一つにして JWT とします。この JWT も DPoP proof JWT です。
![dpop_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/c854281c-8f59-336b-4fe8-4c9d16cab5b9.png)

**（１４）**API コールをおこないます。この時、アクセストークンに加えて、先ほど生成した DPoP proof JWT も含めます。
![dpop_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/6000db45-1b42-303f-3696-19c611dcec93.png)

**（１５）**API の実装は、リクエストからアクセストークンと DPoP proof JWT を取り出します。
![dpop_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/4da2983a-e227-1b71-5bc0-eade640f4bbb.png)

**（１６）**DPoP proof JWT に含まれている公開鍵を用いて、署名を検証します。この検証により、クライアントが当該公開鍵に対応する秘密鍵を持っていることを確認できます。
![dpop_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/096b56ce-3bb4-3cea-9537-81072b2377eb.png)

**（１７）****DPoP proof JWT に含まれている公開鍵とアクセストークンに紐付いているべき公開鍵が同一かどうかを確認**します。同一であることが確認できれば、API コールをしているクライアントが、アクセストークンの正当な所有者（＝アクセストークンの発行を受けたクライアント）であると言えるので、API コールを許可します。一方、同一でなければ API コールを拒否します。
![dpop_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/1cf4df33-11fb-6237-f1c4-fe8cb182ecff.png)

以上です。

**フルカラーの図**
![dpop_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/21c1fa46-071d-86e2-5d31-bef44a25d673.png)


# 2. アクセストークンと公開鍵の紐付け方法

アクセストークンと公開鍵の紐付けは、具体的には、公開鍵のハッシュ値をアクセストークンの属性の一つとして覚えておくことでおこないます。このハッシュ値は、[RFC 7638](https://tools.ietf.org/html/rfc7638 "JSON Web Key (JWK) Thumbprint") で定義されている JWK SHA-256 Thumbprint です。

アクセストークンが公開鍵と紐付いている場合、イントロスペクション API（[RFC 7662](https://tools.ietf.org/html/rfc7662 "OAuth 2.0 Token Introspection")）が返すレスポンスには、その公開鍵のハッシュ値が含まれます。具体的には、公開鍵の JWK SHA-256 Thumbprint を BASE64URL でエンコードした値が、`cnf` クレームの下の `jkt` クレームの値として含まれます。

```json
"cnf": {
  "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
```

アクセストークンのフォーマットが JWT の場合、そのペイロード部分には、同様にして `cnf` クレームの下の `jkt` クレームの値として、公開鍵のハッシュ値が含まれます。


# 3. DPoP proof JWT

## 3.1 DPoP proof JWT ヘッダー

DPoP proof JWT のヘッダー部分の仕様は次の通りです。

|クレーム|要否|説明|
|:--|:-:|:--|
|`typ`|必須|固定値 `"dpop+jwt"`|
|`alg`|必須|署名アルゴリズム。[RFC 7518](https://tools.ietf.org/html/rfc7518 "JSON Web Algorithms (JWA)"), [Section 3.1](https://tools.ietf.org/html/rfc7518#section-3.1 "alg (Algorithm) Header Parameter Values for JWS") の表内の識別子。ただし `"none"` 及び対称鍵系アルゴリズムは不可。|
|`jwk`|必須|クライアントが選択した公開鍵|

次のものは、DPoP の仕様書の Section 4 から抜粋した、DPoP proof JWT のヘッダーの例です。

```json
{
  "typ":"dpop+jwt",
  "alg":"ES256",
  "jwk": {
    "kty":"EC",
    "x":"l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
    "y":"9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA",
    "crv":"P-256"
  }
}
```

## 3.2. DPoP proof JWT ペイロード

DPoP proof JWT のペイロード部分の仕様は次の通りです。

|クレーム|要否|説明|
|:--|:-:|:--|
|`jti`|必須|JWT 一意識別子|
|`htm`|必須|リクエストの HTTP メソッド|
|`htu`|必須|リクエストの URL|
|`iat`|必須|JWT 発行日時|

次のものは、DPoP の仕様書の Section 4 から抜粋した、DPoP proof JWT のペイロードの例です。

```json
{
  "jti":"-BwC3ESc6acc2lTc",
  "htm":"POST",
  "htu":"https://server.example.com/token",
  "iat":1562262616
}
```

例えばトークンリクエスト用の DPoP proof JWT の場合、`"htm"` は常に `"POST"` になり（[RFC 6749](https://tools.ietf.org/html/rfc6749 "The OAuth 2.0 Authorization Framework") で `POST` と決められているため）、`"htu"` はトークンエンドポイントの URL になります。


# 4. トークンリクエスト

先に説明したとおり、公開鍵に紐付いたアクセストークンを生成してもらいたい場合、トークンリクエストに DPoP proof JWT を含めます。その含め方ですが、`DPoP` という HTTP ヘッダーの値として DPoP proof JWT を設定します。

下記は、DPoP の仕様書の Section 5 から抜粋したトークンリクエストの例です。

```http
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik
 VDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCR
 nMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JE
 QSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiItQndDM0VTYzZhY2MybFRjIiwiaHRtIj
 oiUE9TVCIsImh0dSI6Imh0dHBzOi8vc2VydmVyLmV4YW1wbGUuY29tL3Rva2VuIiwia
 WF0IjoxNTYyMjYyNjE2fQ.2-GxA6T8lP4vfrg8v-FdWP0A0zdrj8igiMLvqRMUvwnQg
 4PtFLbdLXiOSsX0x7NVY-FNyJK70nfbV37xRZT3Lg

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
&code_verifier=bEaL42izcC-o-xBk0K2vuJ6U-y1p9r_wW2dFWIWgjz-
```

認可サーバーが DPoP をサポートしている場合、DPoP proof JWT に含まれる公開鍵と生成されるアクセストークンが紐付けられます。そして、トークンレスポンスに含まれる `"token_type"` の値が `"DPoP"` となります。

逆に、認可サーバーが DPoP をサポートしていない場合、DPoP ヘッダーは単に無視され、公開鍵とアクセストークンとの紐付けは行われません。この場合、トークンレスポンスの `"token_type"` の値は `"DPoP"` 以外の値、典型例としては `"Bearer"`、になります。


# 5. API コール

API コールへの DPoP proof JWT の含め方は、トークンリクエストの場合と同様、`DPoP` HTTP ヘッダーを使います。

一方、少し変わるのは、アクセストークンを渡す手段として `Authorization` ヘッダーを使う場合です。

[RFC 6750](https://tools.ietf.org/html/rfc6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage"), [Section 2.1](https://tools.ietf.org/html/rfc6750#section-2.1 "Authorization Request Header Field") に則った従来の方法では、次のようにしてアクセストークンを渡します。

```http
Authorization: Bearer アクセストークン
```

一方、アクセストークンが公開鍵に紐付いている場合は、`Authorization` ヘッダーを使う場合、次のように `Bearer` ではなく `DPoP` を使います。

```http
Authorization: DPoP アクセストークン
```

下記は、DPoP の仕様書の Section 6 から抜粋した API コールの例です。

```http
GET /protectedresource HTTP/1.1
Host: resource.example.com
Authorization: DPoP eyJhbGciOiJFUzI1NiIsImtpZCI6IkJlQUxrYiJ9.eyJzdWI
 iOiJzb21lb25lQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbX
 BsZS5jb20iLCJhdWQiOiJodHRwczovL3Jlc291cmNlLmV4YW1wbGUub3JnIiwibmJmI
 joxNTYyMjYyNjExLCJleHAiOjE1NjIyNjYyMTYsImNuZiI6eyJqa3QiOiIwWmNPQ09S
 Wk5ZeS1EV3BxcTMwalp5SkdIVE4wZDJIZ2xCVjN1aWd1QTRJIn19.vsFiVqHCyIkBYu
 50c69bmPJsj8qYlsXfuC6nZcLl8YYRNOhqMuRXu6oSZHe2dGZY0ODNaGg1cg-kVigzY
 hF1MQ
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik
 VDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCR
 nMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JE
 QSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiJlMWozVl9iS2ljOC1MQUVCIiwiaHRtIj
 oiR0VUIiwiaHR1IjoiaHR0cHM6Ly9yZXNvdXJjZS5leGFtcGxlLm9yZy9wcm90ZWN0Z
 WRyZXNvdXJjZSIsImlhdCI6MTU2MjI2MjYxOH0.lNhmpAX1WwmpBvwhok4E74kWCiGB
 NdavjLAeevGy32H3dbF0Jbri69Nm2ukkwb-uyUI4AUg1JSskfWIyo4UCbQ
```


# 6. Authlete

**[Authlete](https://www.authlete.com/)**（オースリート）はバージョン 2.2 以降で DPoP をサポートします。DPoP をサポートするため、幾つかの Authlete API に DPoP 用のパラメーター群が追加されました。参考までに、追加されたパラメーター群を紹介します。

## /api/auth/token API

|パラメーター|説明|
|:--|:--|
|`dpop`|トークンリクエストに含まれている `DPoP` HTTP ヘッダーの値。|
|`htm`|トークンリクエストの HTTP メソッドの値。デフォルト値は `POST`。|
|`htu`|トークンエンドポイントの URL。デフォルト値はサービスの `tokenEndpoint`。|

トークンリクエストに含まれる `DPoP` ヘッダーの値を `dpop` として `/api/auth/token` API に渡すと、アクセストークンと公開鍵の紐付け処理は Authlete がやってくれます。これだけで DPoP に対応したアクセストークンが生成されます。


## /api/auth/userinfo API

|パラメーター|説明|
|:--|:--|
|`dpop`|ユーザー情報リクエストに含まれている `DPoP` HTTP ヘッダーの値|
|`htm`|ユーザー情報リクエストの HTTP メソッドの値。通常は `GET` もしくは `POST`。|
|`htu`|ユーザー情報エンドポイントの URL。デフォルト値はサービスの `userInfoEndpoint`。|

ユーザー情報リクエストに含まれる `DPoP` ヘッダーの値を `dpop` として `/api/auth/userinfo` API に渡すと、DPoP に必要な検証処理を Authlete がやってくれます。


## /api/auth/introspection API

|パラメーター|説明|
|:--|:--|
|`dpop`|Protected Resource エンドポイントへのリクエストに含まれている `DPoP` HTTP ヘッダーの値|
|`htm`|Protected Resource エンドポイントへのリクエストの HTTP メソッドの値|
|`htu`|Protected Resource エンドポイントの URL。|

Authlete の `/api/auth/introspection` API は、[RFC 7662](https://tools.ietf.org/html/rfc7662 "OAuth 2.0 Token Introspection") が定めるものとは異なる、Authlete 独自のイントロスペクション API です。Authlete のイントロスペクション API は、アクセストークンの情報を返すだけでなく、様々な検証処理をおこない、必要に応じて [RFC 6750](https://tools.ietf.org/html/rfc6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage") に準拠するエラーメッセージの構築もおこないます。この Authlete イントロスペクション API に DPoP 関連のパラメーター群（`dpop`、`htm`、`htu`）を渡すことにより、DPoP 関連の検証処理は全て Authlete がやってくれるようになります。つまり、『1. 図解 DPoP』の（１７）でおこなう「DPoP proof JWT の署名検証」や「アクセストークンと公開鍵の対応チェック」を、Authlete が行います。このため、Protected Resource エンドポイント群の実装はだいぶ楽になります。


# おわりに

PoP の一つである DPoP を紹介しました。SPA など、アプリケーションレベルでの PoP が必要な場合は DPoP の採用を検討してみるとよいでしょう。