May 11, 2020

アプリケーションを oauth2-proxy で保護して curl でアクセスするまで

追記 2020-05-13

この方法に問題があることをご指摘いただきました。本来関係ないクライアントがリソースサーバーにアクセスできる問題がありますので、取り急ぎこの方法は非推奨であることを書いておきます(では、どのようにすればいいのかというところをまた後日追記します)。

追記 2020-05-18

結論としてはこの方法は ID Token の扱いとしてはふさわしくなく、利用すべきでないと思い直しました。

自分の言葉足らずなところがありましたが、この方法は自身が管理している Internal 向けのアプリケーションに対して施すことを想定して書きました。
上記のツイートにリプライしていますが、Internal 向けで Client ID / Secret も漏洩しにくい状況で、認証するメールアドレスはドメインを絞った上であれば aud の検証が oauth2-proxy で行われている以上、セキュリティリスクは低いと思ってのことでした。

で、次の記事でもご意見をいただきました(この記事で参照されている記事やコメントが大変参考になります)。

ID Token ではなくアクセストークンを使えというのはその通りであり、ID Token はセッション目的で使うものではないので、セッションはまた別で生成する必要があると思います。
ID Token もそれだけで IdP 側から情報を取得できるので、それを refresh を利用して多用するよりも認証後にセッションを自前で張る方が安全でしょう。

https://developers.google.com/identity/protocols/oauth2/openid-connect には次のようにあります。

After obtaining user information from the ID token, you should query your app’s user database. If the user already exists in your database, you should start an application session for that user if all login requirements are met by the Google API response.

application session なので ID Token をそのまま使うのではなく、自分たちで張ってくれという意味でしょう。
ということで、この方法はあまり良くないと思い直すに至りました。

一方で oauth2-proxy がこのような実装にしている理由(自分が調べきれていないだけで実はそこも自分たちで実装すると言っている?)や、例えば Kubernetes でも ID Token をセッションとして使っているのですが(自分は使ったことないがドキュメント https://kubernetes.io/docs/reference/access-authn-authz/authentication/ を見る限りはそのようにある( To identify the user, the authenticator uses the id_token (not the access_token) from the OAuth2 token response as a bearer token. See above for how the token is included in a request.)) ので、これは……という気持ちが残っていますので、もう少しここを調べてみようと思います。


oauth2-proxy 便利ですよね。自分は pomerium 推しになってしまったのですが、nginx-ingress でもシュッと使えるのは魅力的です。

さて、その oauth2-proxy ですが、いわゆる REST API リソースを保護した上で curl からサクッと叩きたいというときがあります。
ドキュメントにそれらしいものはないのですが、OAuth2 なので token さえ取得できればいいので何か方法はあるだろうとオプションを眺めたところ、 --extra-jwt-issuers というそれらしいオプションがありました。

導入されたPR は https://github.com/oauth2-proxy/oauth2-proxy/pull/65 です。
オプションと PR コメントをパッと見ても「どう設定すればええんや…」となると思うので、ここにメモしておきます。

流れとしては OAuth2 Client を「その他のアプリケーション」として作り、そのクレデンシャルを利用して Google から直接 JWT を取得して、それをヘッダに付与して oauth2-proxy で保護されているアプリケーションに送信する感じになります。


Google API で Client を作る

https://oauth2-proxy.github.io/oauth2-proxy/auth-configuration#google-auth-provider にあるように OAuth Client を作ります。
ここではアプリケーションの種類として「Web application」を選択しています。ここで発行されたクレデンシャルは OAuth2 Proxy で使いますので、控えておきます。仮に CLIENT_ID=AAAA.apps.googleusercontent.com , CLIENT_SECRET=BBBB としましょう。

さらにもう一つ OAuth Client を作り、種類は「その他のアプリケーション」を選択してください。
この Client は Google から直接 JWT を取得するために使います。ここで発行されたクレデンシャルを仮に CLIENT_ID=XXXX.apps.googleusercontent.com , CLIENT_SECRET=YYYY とします。

oauth2-proxy に設定する

oauth2-proxy のドキュメントにあるように「Web application」として発行したクレデンシャルは、それぞれ --client-id--client-secret の値として渡します。
そして、JWT でのアクセスを可能にするために --extra-jwt-issuers には「その他のアプリケーション」で作った OAuth Client の CLIENT_IDhttps://accounts.google.com=$CLIENT_ID というフォーマットで値として設定します。
また、JWT で認証を通すために --skip-jwt-bearer-tokens=true を指定します。

一応 docker-compose.yml を置いておきます。適当にオプションの値を変更してください。

JWT を取得する

これは https://cloud.google.com/iap/docs/authentication-howto?hl=ja#signing_in_to_the_application にある通りに進んでください。

https://accounts.google.com/o/oauth2/v2/auth?client_id=XXXX.apps.googleusercontent.com&response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob という URL を開き、認可します。
するとトークンが発行されるので、そのトークンを JWT を取得するエンドポイントに送信します。

❯ curl --data client_id=XXXX.apps.googleusercontent.com --data client_secret=ZZZZ --data code=$AUTH_TOKEN --data redirect_uri=urn:ietf:wg:oauth:2.0:oob --data grant_type=authorization_code https://oauth2.googleapis.com/token

すると次のようなレスポンスが返ってきます。

{
  "access_token": "...",
  "expires_in": 3599,
  "refresh_token": "...",
  "scope": "https://www.googleapis.com/auth/userinfo.email openid",
  "token_type": "Bearer",
  "id_token": "eyJhGc..."
}

この id_token が JWT になっていますので、これを Authorization: Bearer $ID_TOKEN というヘッダにして oauth2-proxy で保護されているアプリケーションへ送信するだけです。

❯ curl -H 'Authorization: Bearer eyJh...' -k https://app.example.com

デフォルトでは1時間で JWT の有効期限が切れてしまうので、 refresh_token を保存しておいて refresh する必要があると思います。

❯ curl --data "refresh_token=$REFRESH_TOKEN" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token

有効期限の残り時間やスコープは curl "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=$ACCESS_TOKEN" で確認ができ、token の revoke は curl https://accounts.google.com/o/oauth2/revoke?token=$TOKEN ( $TOKEN は Access Token と Refresh Token のどちらでも OK) でできます。


これで oauth2-proxy で保護されたアプリケーションでも Bot や CLI アプリケーション等を通して扱えるようになりました。
最初だけブラウザを立ち上げる必要がありますが、これは Cloud SDK と同じ話でもありますね。無限に token を refresh していると、トークンが漏洩したときに気づきにくいので、GSuite の監査ログで監視しておくと良いと思います。

追記

これトークンを使い回すのは正しいのだっけ…? となったので調べている


このエントリーをはてなブックマークに追加

© Kohei Morita 2020