Ktor で Google アカウントの OpenID Connect を実装する

Ktor で Google アカウントの OpenID Connect を実装したときに参考にしたドキュメントや、利用したライブラリについてまとめる。

## 実装方針

基本的には、以下の2つのドキュメントに沿って実装する。

- https://ktor.io/docs/oauth.html
- https://developers.google.com/identity/openid-connect/openid-connect

Ktor のドキュメントに記載のとおりに `oauth` プロバイダーを実装すれば、ローカル環境で OAuth を動作させることは簡単だ。
まさに Google アカウントで OAuth を動作させる[サンプルコード](https://github.com/ktorio/ktor-documentation/tree/2.2.2/codeSnippets/snippets/auth-oauth-google)もあるので、合わせて確認すると良い。

ただ、 Ktor のドキュメントはサンプル用であり、追加で以下の対応をした。

- OpenID Connect で認証できるようにする
- OAuth 2.0 および OpenID Connect のセキュリティ面で必要な対応をする
    -  state の検証
    -  nonce の検証
    -  IDトークン の検証
-  OpenID Connect ディスカバリーの利用
-  OpenID Connect 認証後のIDトークンの保存や利用

このうち、 OpenID Connect 認証後のIDトークンの保存や利用については、普通にデータベースに保存したりするだけなので扱わない。
他の点について個別に記載する。

## OpenID Connect で認証できるようにする

Ktor が提供しているのは OAuth だけなので、その上で OpenID Connect で認証できるように実装する必要がある。
といっても、前述の Google の[ドキュメントどおり](https://developers.google.com/identity/openid-connect/openid-connect#sendauthrequest)に実装するだけではある。

`oauth` プロバイダーの `providerLookup` に以下のような指定をする

- scope に openid を指定する（メールアドレスが必要な場合は email も）
- リクエストメソッドを GET にする
- client_id や client_secret、 redirect_uri を指定する

## セキュリティ面の対応

### state の検証

OAuth 2.0 の仕様に従って、適切に state を設定した上で Google から返ってきた state を検証する必要がある。

`oauth` プロバイダーではこの state の生成と検証を `providerLookup` で指定する `nonceManager` が担当している。
`nonceManager` の `nonce` は、 OpenID Connect で ID トークンに含める nonce ではなく、一回限りのデータという一般的な意味での nonce を指している。
OpenID Connect で利用するときは混同しやすいので注意。

デフォルトでは、 `GenerateOnlyNonceManager`([コード](https://github.com/ktorio/ktor/blob/23339b12427d44b79f1b64e494390148d04b1528/ktor-utils/common/src/io/ktor/util/NonceManager.kt#L31))が指定されており、 state が検証されていない。
常に検証には成功してしまう。

Ktor に含まれる範囲では、 `StatelessHmacNonceManager`（[コード](https://github.com/ktorio/ktor/blob/23339b12427d44b79f1b64e494390148d04b1528/ktor-utils/jvm/src/io/ktor/util/StatelessHmacNonceManager.kt)）でいちおう検証できる。
より正確には、 Redis なり DB なりに state を保存して検証すべきだろう。

### nonce の検証

OpenID Connect の ID トークンに含まれる nonce も生成、保存、リクエストのパラメータに付与、検証の必要がある。
こちらは `oauth` プロバイダーの管轄外なので独自に実装する。

nonce の生成には、 Ktor に含まれる `generateNonce`（[コード](https://github.com/ktorio/ktor/blob/6763a7344059c1f9dfdb2f0816e98559862ef604/ktor-utils/jvm/src/io/ktor/util/CryptoJvm.kt#L64)）も利用できる。
これは16文字のランダムな文字列を生成する。

nonce の保存は自分で管理する必要がある。
後々の検証のため、 nonce は state と紐付けておくと便利だ。
これには、 `providerLookup` の `onStateCreated` で生成および保存をしておくのが良さそうだった。

nonce をリクエストパラメータに付与する際には、 `providerLookup` の `authorizeUrlInterceptor` でリクエストをインターセプトする。
この際に、先程保存しておいた nonce を早速利用する。
実装例は以下の通り。

```kotlin
// nonceMap: Map<String, String> に state をキーにして nonce が保存されているとする
authorizeUrlInterceptor = interceptor@{
                    val state = parameters["state"] ?: return@interceptor
                    val nonce = nonceMap[state] ?: return@interceptor
                    parameters.append("nonce", nonce)
                },
```

nonce の検証は次の ID　トークンの検証で行うため、そちらで説明する。

### ID トークンの検証

ID トークンの検証は [公式ドキュメント](https://developers.google.com/identity/openid-connect/openid-connect?hl=ja#validatinganidtoken) に記載の内容に従う。
上記ドキュメントでは署名、iss、aud、expを確認している。
加えて、nonceも確認する。

Ktor と組み合わせて使う際に、個人的には以下のライブラリを利用した。
このライブラリでは検証用のロジックを容易に実装することができた。

- https://github.com/auth0/java-jwt

公開鍵の検証の際には、 OpenID Connect のディスカバリドキュメントにも記載されている jwks_uri を利用できる。
ディスカバリドキュメントの利用については次の章で説明するが、それによって jwks_uri を得られれば、次のライブラリを利用することで、

- https://github.com/auth0/jwks-rsa-java

## OpenID Connect ディスカバリーの利用

ディスカバリドキュメントには、IDトークンの検証の際に利用した jwks_uri の他、 authorization_endpoint など、 OpenID Connect で利用する各種リソースが記載されている。
ディスカバリドキュメントを利用することで、それぞれのリソースをハードコーディングする必要がなくなり、ディスカバリドキュメントのURLだけ管理すればよくなる。

具体的なディスカバリドキュメントのURLは以下。

- https://accounts.google.com/.well-known/openid-configuration

実装の際には、 Ktor Client でディスカバリドキュメントから情報を取得する。