APIの認証をEntra ID(旧Azure AD)で行う

APIで認証を行う場合、アプリケーション側で作り込む方法もなくはないが、アプリケーション側で作り込むとAPIが乱立した際に個々に実装するのは手間でありメンテも大変なので、API Gateway側で認証を行うことが一般的である。
Kong GatewayをAPI Gatewayとして使った場合、[OpenID Connectプラグイン](https://docs.konghq.com/hub/kong-inc/openid-connect/)（以下OIDCプラグイン）を使えば外部の認証サービスと連携して簡単にGateway側に認証の機能をもたせることが出来る。
今回はOIDCプラグインの設定方法や挙動の確認のために、素のnginxをEntra ID(旧Azure AD)の認証を使ってアクセスするようにしてみる。
なお、[こちら](https://docs.konghq.com/hub/kong-inc/openid-connect/how-to/third-party/azure-ad/)のドキュメントを参考に検証した。

## 前提条件
以下があるものする。
- Kong Gateway
- サンプルアプリ＆サービス
- 上記のアプリ＆サービスに対応するService・RouteをKong Gateway上で作成済み

今回サンプルアプリとサービスは[Kong Gatewayの通信の遅延箇所をJaegerで確認する](https://qiita.com/ipppppei/items/adb17cf20e13a786d283)の記事で使った[こちらのnginx](https://qiita.com/ipppppei/items/adb17cf20e13a786d283#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%A8serviceroute%E3%81%AE%E4%BD%9C%E6%88%90)を使用する。
なお、サービスはTLSで暗号化されている必要がある。
サンプルアプリのサービスについてブラウザでアクセスし証明書を確認すると、Let's Encryptを使った証明書で接続していることが確認できるのでこれで進める。
![20240603101651.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/e1ff10cb-9dbf-0a2b-4860-33f5660bde1a.png)

また、サンプルアプリのホスト名を後で使うため、ここで環境変数に設定しておく。
```sh
MYHOST=nginx.eks.hogehoge.com
```
※前の記事を読んでいないと分かりづらいが、このアドレスはProxyからアクセスするRoute設定であり、localhostに立てた場合は`localhost:8000/<Routeのpath>`部分になる。

## 事前準備
### Entra IDの設定
OIDCプラグインを設定するにはEntra IDの以下の項目が必要となる。
- Issuer
- クライアントID(アプリケーションID)
- クライアントSecret(シークレットIDに対応する値)

これらの値を取得するためにEntra ID側でアプリケーションを作成して各値を取得する。
アプリケーションの作成方法についてはここでは割愛する。手順が必要な場合は過去に書いた[こちら](https://qiita.com/ipppppei/items/7da1349bb50532b94f54#azure%E5%81%B4%E3%81%AE%E8%A8%AD%E5%AE%9A)の記事を参考にすると良い。

環境変数に取得した値をそれぞれ設定する。
```sh
AZURE_ISSUER=https://login.microsoftonline.com/f177c1d6-50cf-49e0-818a-xxxxxx/v2.0
AZURE_CLIENT_ID=36cd0496-ab59-4ffd-8925-xxxxxx
AZURE_CLIENT_SECRET=U_u8Q~Jq3RRbYixxxxx
```

また、テナントIDが必要になるのでIssuerから引っこ抜く形で以下で設定しておく。
```sh
AZURE_TENANT_ID=$(sed "s@.*.com/\(.*\)/v2.0@\1@g" <<< "$AZURE_ISSUER")
```

## OIDCプラグインの設定
Admin APIのエンドポイントを環境変数に設定する。
```sh
ADMIN_API=https://kong.hogehoge.com/api
```

サンプルアプリのService`MYHOST`に対応するRouteのIDを取得する。
```sh
ROUTE_ID=$(curl -k -X GET ${ADMIN_API}/routes | jq -r --arg host "$MYHOST" '.data[] | select(.hosts != null) | select(.hosts[] == $host) | .id')
```
念の為値が入っているか確認する。
```sh
$ echo $ROUTE_ID
da7eea27-3de1-552b-8f18-xxxxx
```
[公式の手順](https://docs.konghq.com/hub/kong-inc/openid-connect/how-to/third-party/azure-ad/#plugin-configuration)を参考にAdmin API経由でRouteに対してOIDCプラグインを有効化する。
```sh
curl -i -k -X POST ${ADMIN_API}/routes/${ROUTE_ID}/plugins \
  --data name="openid-connect" \
  --data config.issuer="${AZURE_ISSUER}/.well-known/openid-configuration" \
  --data config.issuers_allowed="https://sts.windows.net/${AZURE_TENANT_ID}/" \
  --data config.client_id="$AZURE_CLIENT_ID" \
  --data config.client_secret="$AZURE_CLIENT_SECRET" \
  --data config.redirect_uri="https://$MYHOST" \
  --data config.scopes="openid" \
  --data config.scopes="email" \
  --data config.scopes="profile" \
  --data config.scopes="${AZURE_CLIENT_ID}/.default" \
  --data config.verify_parameters="false"
```

なお、公式ドキュメントの手順だと`config.issuers_allowed`は含まれていないが、これを指定しないと以下のようなエラーが出る（`log_level`に`debug`を指定しないと出力されないので注意）。
```sh
 invalid issuer (https://sts.windows.net/xxxxx/) was specified for access token, unknown error (not a table) was expected
```
これの回避策については[こちらのナレッジ](https://support.konghq.com/support/s/article/Open-ID-Connect-plugin-fails-with-invalid-issuer-error-when-authenticating-with-Azure-AD)に記載があり、これに従う形でパラメタ`config.issuers_allowed`で追加している。


## 動作確認
### ブラウザでの確認
ブラウザのキャッシュを削除し、先程と同じようにブラウザ経由でアクセスしてみる。
```sh
open https://$MYHOST
```
すると以下のようにMicrosoftの認証画面になる。
![20240603114120.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/75f8f05b-c6a0-9891-8fc4-fdf9893dcacb.png)

認証突破後、無事nginxの画面が見れればOK。
![20240604150941.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/cb0d5bc6-6668-9748-3969-d251b47c8751.png)

上記のようにただのnginxのアクセスに簡単にEntra IDの認証を設定することが出来た。

### curlでの確認
APIの認証なので`curl`で出来ないとあまり意味がない。
ということで`curl`でアクセスするためにIntra IDからアクセストークンを取得する。
グラントタイプを`client_credentials`でトークンを取得した。
```sh
BEARER_TOKEN=$(curl -X POST "https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token" \
 --data scope="${AZURE_CLIENT_ID}/.default" \
 --data grant_type="client_credentials" \
 --data client_id="${AZURE_CLIENT_ID}" \
 --data client_secret="${AZURE_CLIENT_SECRET}"　\
 | jq -r '.access_token')
```
curlを実行する。最初はトークンなしで試してみる。
```sh
$ curl -i https://$MYHOST
HTTP/2 302
：（省略）
```
MSの認証サイトに飛ばされるので302が返る。
トークンをつけてみる。
```sh
$ curl -i https://$MYHOST -H "Authorization: Bearer $BEARER_TOKEN"
HTTP/2 200
：（省略）
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
：（省略）
```
無事アクセスできた。問題なさそうだ。

## 補足（追記）
この記事では手を抜いて`config.issuers_allowed`に1つのURLしか指定していないが、本来は[KB](https://support.konghq.com/support/s/article/Open-ID-Connect-plugin-fails-with-invalid-issuer-error-when-authenticating-with-Azure-AD)にあるように3つ書いた方がいい。
この記事を書いてしばらく経った後に同じ手順で設定したところ、認証突破後に以下のエラーが表示されてUpstreamのサービスに接続できなかった。
```json
{"message":"Unauthorized}
```
`config.display_errors="true"`を追加して再度プラグインを有効化してエラーを確認すると、以下のように表示されて記載漏れが確認できた。
```json
{"message":"Unauthorized (required issuers were not found [ https://login.microsoftonline.com/f177c1d6-50cf-49e0-818a-xxxxxx/v2.0 ])"}
```
なので、`issuers_allowed`はちゃんと書こうというのと、`config.display_errors="true"`は開発環境とかであればとりあえず付けておくといいと思う。