OAuth2-Proxyの挙動を調べた

[OAuth2-Proxy](https://github.com/oauth2-proxy/oauth2-proxy)がどのような振る舞いをするのか気になったので調査しました。

## 前提

- Versionはv7.7.1です
- `reverse_proxy = true` で動作させています
  - Nginxとインテグレーションして動かせますが、今回の調査ではReverse Proxyとして動作させています
  - https://oauth2-proxy.github.io/oauth2-proxy/configuration/integration/
- 全て localhost で動作させた内容です
    - `cookie_secure = false` を指定した挙動です
- IdP には Auth0 を使っています
- トークンの保存先にRedisは使っていません

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10499/15975b87-2fe8-bef4-b736-c8c618236f00.png)



## IdPから取得したトークンはどこに保存される？

2種類から選択できるみたいです。
https://oauth2-proxy.github.io/oauth2-proxy/configuration/session_storage

- Cookie
- Redis

Cookieの場合、IdPから取得した各種トークンが `cookie-secret` で指定した対称鍵により暗号化されているみたいです。デフォルトではCookieのNameは `_oauth2_proxy` となっていました。


## 認可リクエストのクエリパラメーターはどうなる？

Authorization Code Flowになると想定しつつ、実際の挙動を調査しました。

### デフォルトの場合

予想通りAuthorization Code Flowでした。セキュリティに関するパラメーターは `state` のみ指定でした。PKCEは自動でやらないようです。

```
https://example.jp.auth0.com/authorize
  ?approval_prompt=force
  &client_id=2KqFbAjwMB46U......
  &redirect_uri=http://localhost:4180/oauth2/callback
  &response_type=code
  &scope=openid email
  &state=123abc
```

気になったのはデフォルトで `approval_prompt=force` が付与されていたことでしょうか。GoogleをIdPとして意識した場合のパラメーターのようです。実害はなさそうなのでスルーで良さそうです。
https://github.com/oauth2-proxy/oauth2-proxy/issues/191


### PKCEを使う方法

`code_challenge_method=S256` をセットすれば動きました。セキュアになるので、なるべく指定するほうが良さそうです。


## IdP固有パラメーターの設定方法

### 背景

Auth0には認可リクエストにおいて `audience` という固有のクエリパラメーターがあります。RFC8707のResource Indicatorに近いパラメーターです。これを認可リクエストに指定することで、どのバックエンドサーバーに向けたアクセストークンを発行するか指定可能です。

また、他にもAuth0 OrganizationsというマルチテナントSaaSを実装するための便利な機能があります。どのようなログイン体験を提供するかに依存しますが、認可リクエスト時に `organization` や `connection` などのパラメーターを指定することもあります。
https://auth0.com/docs/manage-users/organizations

標準仕様に存在しない独自パラメーターの場合のことを調査しました。

### デフォルト

openid-configurationの値を見て、認可リクエストエンドポイントやトークンエンドポイントを取得しています。openid-configurationを利用する場合、OAuth2-Proxyには `audience` や `organization` を指定する設定はありませんでした。

回避策として、openid-configurationを参照せず、それぞれのエンドポイントのURLを個別に指定する必要がありました。
https://github.com/oauth2-proxy/oauth2-proxy/issues/1250

```
skip_oidc_discovery = true
login_url = "https://example.jp.auth0.com/authorize?audience=https://example.api"
redeem_url = "https://example.jp.auth0.com/oauth/token"
oidc_jwks_url = "https://example.jp.auth0.com/.well-known/jwks.json"
```

`login_url` に `audience` を固定値として埋め込む方法です。あまり美しくありませんが、こうせざるを得ないみたいです。

この方法だと静的パラメーターには対応できますが、 `organization` などユーザーごとで動的に切り替わるパラメータには対応できません。（もちろん、 `audience` も用途によっては動的に変更したいことがありそうです）
現状だとOAuth2-Proxyをforkしてカスタマイズするしか方法はなさそうです。


## バックエンドにトークンを渡す方法

### デフォルト

バックエンドサーバーへのリクエストにはトークンが入りません。


### pass_access_token = true

`pass_access_token = true` を指定するとアクセストークンが x-forwarded-access-token Header にセットされました。バックエンドサーバーでもリクエストを検証する場合は、この設定はしたほうがいいでしょう。

ヘッダーが `Authorization: Bearer` ではなく `x-forwarded-access-token` になっている点は気になりました。これは私の想像ですが、`Authorization` ヘッダーはBasic認証など他の認証とコンフリクトする可能性があるため、あえて別名のヘッダーにしているのではないかと推測してます。


### pass_authorization_header = true

`pass_authorization_header = true` を指定すると、IDトークンが `Authorization: Bearer` でセットされ、バックエンドサーバーにリクエストが届きました。

私の想定だとアクセストークンの方が `Authorization: Bearer` に指定されるのかなと思っていたのですが、この挙動は意外でした。これは OAuth2-Proxy の設計思想に答えがありそうです。以下をそのうち読んでみようと思います。
https://github.com/oauth2-proxy/oauth2-proxy/issues/843


## トークンとCookie有効期限の関係（リフレッシュトークンなし）

まず Cookie の有効期限に関するパラメーターを調べました。リフレッシュトークンなしで動かしてみます。

### cookie_expire

set-cookieされてから期限が切れるまでの時間を指定するようです。試しに `100s` を指定すると、 `/oauth/callback` のレスポンスにてセットされた Cookie の有効期限が100秒後になってました。

### cookie_refresh

設定ファイルのexampleにドキュメントより丁寧な説明コメントがありました。

```
## Refresh  - (duration) refresh the cookie when duration has elapsed after cookie was initially set.
##            Should be less than cookie_expire; set to 0 to disable.
##            On refresh, OAuth token is re-validated.
##            (ie: 1h means tokens are refreshed on request 1hr+ after it was set)
```

### 試してみる

トークンの有効期限が十分に長い状態で `cookie_expire = 100s` `cookie_refresh = 10s` にしてみました。Cookieの有効期限はいくらブラウザを更新しても、更新されることがありませんでした。トークンを再検証すると書いてあるので、アクセストークンの有効期限が長い場合には影響を受けないみたいです。

今度はアクセストークンの有効期限を短くしてみました。この場合はアクセストークンの有効期限切れのタイミングでアクセスすると、再認証が求められました。
（access_token=20s, cookie_refresh=10s, cookie_expire=100s）

では、アクセストークンの有効期限は十分長いまま、IDトークンの方を短くするとどうなるか？同じように、IDトークンの有効期限切れのタイミングでアクセスしたら再認証が要求されました。どうやらアクセストークン、IDトークンどちらかの有効期限が短い方を優先するみたいです。違和感のない挙動です。
（id_token=20s, cookie_refresh=10s, cookie_expire=100s）

そのため、リフレッシュトークンを利用しない場合だと以下のようにしておくのが良さそうです。

- cookie_expireはトークンの有効期限より短い時間にしておく
    - トークンの有効期限が切れているが、Cookieはまだ生きているという状況を考慮しないようにする
- cookie_refreshはcookie_expireの数値を鑑みて適度な感覚にしておく
    - 安全面に倒すなら短めくしておくのがいいかも
    - JWTの検証によるレイテンシーが気になるなら頻度を少なくしておくとか


## トークンとCookie有効期限の関係（リフレッシュトークンあり）

ではリフレッシュトークンを使うとどうなるでしょうか？以下の設定で試してみます。（アクセストークンの有効期限は十分に長いです）

- cookie_expire = 3m
- cookie_refresh = 10s
- id token exp = 1m
- refresh token exp = 2m

この場合、cookie_refreshで指定した時間にリクエストを行うと、IDトークンの有効期限が切れていない場合に関わらず、リフレッシュトークンによる洗い替えが行われました。また、Cookieの有効期限も延長がされました。

そのため、リフレッシュトークンを利用する場合は、以下のような設定にしておくのが良さそうです。

- cookie_expire
    - ユーザーのインアクティブな状態が続いた場合に、セッションをクリアする時間
- cookie_refresh
    - 指定された時間が経過した場合、アクセストークン・IDトークンの有効期限に関わらずトークン洗い替えを行う
    - あまりに短い時間に設定すると、無駄にリフレッシュしてIdPへ負荷をかける可能性がある
    - アクセストークン・IDトークンの有効期限が切れたとしても、リフレッシュトークンさえ生きていればトークン再取得が可能なので、アクセストークン・IDトークンの有効期限を鑑みて設定をしたほうがいい