【keycloak on Fargate 2】keycloakとALB認証でWebアプリケーションを保護

# やること
- https://qiita.com/yuta0003/items/c945fd89578b9f64bf2d
にて作成したkeycloakをALB認証に使ってみる
- ALB認証(OIDC)によって、後段のサーバに渡すヘッダをのぞいてみる
- AWS公式リファレンスに記載されている前提事項は以下の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/a6423a92-06d7-7df1-471b-89f191181ee2.png)
- イメージ的にはこんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/393e11a7-0048-d14b-69e0-df7a83db3a6c.png)



# 本記事で作成するリソース
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/6b40d562-20f8-5641-f54e-9f3db3286041.png)

# keycloakのOpenID Endpointを控えておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/7ded8fad-c1a3-70ae-2e82-2b6553d078d3.png)
keycloakにログインし、「Realm settings」メニューを開き、「OpenID Endpoint Configuration」を押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/694ad7ee-6469-ae7d-3005-8096177dbcea.png)
別タブにてJson形式で情報が表示されるため、「issuer」、「authorization_endpoint」、「token_endpoint」、「userinfo_endpoint」の値を控えておく。

# keycloakのクライアント作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/5157fb40-2784-9e1e-384d-faa22ae971ce.png)
「Clients」メニューを開き、「Create client」を押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/26ded750-ab21-438c-8db6-47b9ab3e2ac8.png)
Client IDは任意の名前を記載。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/58e26f31-ec19-fdd3-4db4-8f2ff2159642.png)
Client authenticationはオンにする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/9752e786-fdd3-529f-55ee-0ca980aa5eec.png)
Root URL、Home URLには
```
https://<webサーバドメイン>
```
Valid redirect URIsには
```
https://<keycloakサーバドメイン>/oauth2/idpresponse
```
を記載する。
ちなみにこの「/oauth2/idpresponse」はALB側で使うものであり、ユーザが実装する必要はない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/4761bec9-f444-6eb4-168e-9e9981c02294.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/98fdac10-1ce8-23c7-77d6-06233d20623c.png)
作成したクライアントの「Credentials」タブより、Client Secretをコピーしておく。

# EC2インスタンスの作成
保護対象となるEC2インスタンスを作成する。
今回はnginx+phpの構成で簡易的なWebサーバを構築。
セキュリティグループは次に作成するALBからのHTTP接続を許容する設定で作成して割り当てる。

<details><summary>EC2にnginx、phpをインストール</summary>
nginxのインストール

```
[ec2-user@ip-xxx ~]$ sudo yum update -y
[ec2-user@ip-xxx ~]$ sudo yum -y install nginx
[ec2-user@ip-xxx ~]$ sudo systemctl enable nginx
[ec2-user@ip-xxx ~]$ sudo systemctl start nginx.service
[ec2-user@ip-xxx ~]$ sudo systemctl status nginx.service
● nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabl>
    Drop-In: /usr/lib/systemd/system/nginx.service.d
             └─php-fpm.conf
     Active: active (running) since Wed 2024-07-31 15:23:53 UTC; 4 days ago
   Main PID: 22747 (nginx)
      Tasks: 2 (limit: 1114)
     Memory: 3.3M
        CPU: 5.876s
     CGroup: /system.slice/nginx.service
             ├─ 22747 "nginx: master process /usr/sbin/nginx"
             └─158598 "nginx: worker process"
```
phpのインストール

```
[ec2-user@ip-xxx ~]$ sudo wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
[ec2-user@ip-xxx ~]$ sudo yum install php -y
```

/etc/nginx/nginx.confの編集
以下のlocation設定を追加

```/etc/nginx/nginx.conf
server {
    (省略)
     location ~ \.php$ {
            root           /usr/share/nginx/html;
            fastcgi_pass    unix:/run/php-fpm/www.sock; # こちら派。パスの設
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME　$document_root/$fastcgi_script_name;
            include        fastcgi_params;
        }
}
```
再起動

```
[ec2-user@ip-xxx ~]$ sudo nginx -s reload
```

/etc/php-fpm.d/www.confの編集
以下のlisten設定を編集

```/etc/nginx/nginx.conf
<省略>
listen.owner = nginx
listen.group = nginx
```
再起動

```
[ec2-user@ip-xxx ~]$ sudo service php-fpm restart
```

indexファイルを設置

```
[ec2-user@ip-xxx ~]$ cd /usr/share/nginx/html
[ec2-user@ip-xxx ~]$ touch index.php
[ec2-user@ip-xxx ~]$ sudo vi index.php
```
中身は何でもよいが、ヘッダ情報を取得するため以下の通り記載

```index.php
<?php
ini_set('mbstring.internal_encoding' , 'UTF-8');
?>

<html>
  <head>
    <title>Welcome to nginx!</title>
    <style>
      html {
        color-scheme: light dark;
      }
      body {
        width: 80%;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
      }
      table {
        width: 100%; /* テーブルが親要素の幅に合わせる */
        border-collapse: collapse; /* テーブルのボーダーを結合 */
      }
      th, td {
        border: 1px solid #ddd; /* ボーダーを追加 */
        padding: 8px; /* 内側の余白 */
        text-align: left; /* テキストを左寄せ */
        overflow-wrap: break-word; /* テキストの折り返し */
      }
      th {
        background-color: #f2f2f2; /* ヘッダーの背景色 */
      }
      .table-container {
        max-width: 100%; /* コンテナの最大幅を100%に設定 */
        overflow-x: auto; /* 横方向のスクロールを可能にする */
      }
    </style>
  </head>
  <body>
    <h1>ALB認証成功！！</h1>
    <h2>認証に係るHTTPヘッダ</h2>
    <div class="table-container">
    <table>
      <tr>
        <th>アクセストークン</th>
        <td>HTTP_X_AMZN_OIDC_ACCESSTOKEN</td>
        <td>
          <?php print_r($_SERVER['HTTP_X_AMZN_OIDC_ACCESSTOKEN']) ?>
        </td>
      </tr>
      <tr>
        <th>UUID</th>
        <td>HTTP_X_AMZN_OIDC_IDENTITY</td>
        <td>
          <?php print_r($_SERVER['HTTP_X_AMZN_OIDC_IDENTITY']) ?>
        </td>
      </tr>

      <tr>
        <th>ユーザークレーム (JWT)</th>
        <td>HTTP_X_AMZN_OIDC_DATA</td>
        <td>
          <?php print_r($_SERVER['HTTP_X_AMZN_OIDC_DATA']) ?>
        </td>
      </tr>
    </table>
    </div>
```
</details>

# ドメイン、証明書作成
WebサーバにHTTPS接続するために、ドメインと証明書を作成しておく。

# 認証を行うALBを作成
コンテナの前段に配置するALBを作成する。
▷ターゲットグループ
| # | ## |  |
|:-:|:-:|:-:|
| 基本的な設定 | ターゲットタイプの選択 |インスタンス|
|   | ターゲットグループ名 | 任意の名称  |
|   |プロトコル : ポート|HTTP: 80|
|   |IP アドレスタイプ| IPv4|
|   |VPC| 作成済みのVPCを選択 |
|   |プロトコルバージョン|HTTP1|
|ヘルスチェック|ヘルスチェックプロトコル| HTTP|
|   |ヘルスチェックパス|/|
|ターゲット||先ほど作成したEC2インスタンスを選択|

▷ロードバランサー
| # | ## |  |
|:-:|:-:|:-:|
|ロードバランサータイプ| |Application Load Balancer |
|基本的な設定|ロードバランサー名| 任意の名称  |
|   |スキーム|インターネット向け|
|ネットワークマッピング |VPC|作成済みのVPCを選択|
|   |VPC| 作成済みのVPCを選択 |
|   |マッピング|パブリックサブネットを二つ選択する|
|セキュリティグループ ||作成済みのSGを選択|
|リスナーとルーティング|リスナー|プロトコル：HTTPS／ポート：443／ターゲットグループ：作成済みのターゲットグループを選択|
|セキュアリスナーの設定 |セキュリティポリシー|ポリシー名：ELBSecurityPolicy-TLS13-1-2-2021-06|
|   |デフォルト SSL/TLS サーバー証明書|作成済みのACM証明書を選択|

★認証設定★
作成したリスナーを選択し、「リスナーの編集」を押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/77ed4d59-6f1e-a578-7bf1-ca33573d36de.png)
先程控えた各値を記載し、保存する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/71bf8b9c-5679-e861-425a-e45a74839fb4.png)

# レコードセット登録
Route53にてエイリアスレコードを使用して、作成したドメインとALBを紐づける。

# 動作確認
Webサーバにアクセスしてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/4f5b87e0-5a4e-2419-14f8-207ae3190b31.png)
realmユーザの認証情報を用いてログイン。
下記の通り、各ヘッダが表示されていればOK！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/4927f8cf-39ca-6837-5479-9cb0a1ccd6c3.png)
各ヘッダの役割は以下の通り。
|ヘッダ|役割|
|:-:|:-:|
|HTTP_X_AMZN_OIDC_ACCESSTOKEN|トークン エンドポイントからのアクセス トークン|
|HTTP_X_AMZN_OIDC_IDENTITY|ユーザー情報エンドポイントに含まれるsubクレーム|
|HTTP_X_AMZN_OIDC_DATA|ユーザークレーム|

subクレームにはkeycloakのユーザIDが入るようですね。
せっかくなのでアクセストークンをデコードしてみます。
■ ヘッダ
```
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "G_U4yHAZfrrPi9a3e7lQG6gTi4jXjZKxThEHSPLaUCY"
}
```
■ ペイロード(個人情報はxxxでマスクしてます)
```
{
  "exp": 1722876090,
  "iat": 1722876030,
  "auth_time": 1722876029,
  "jti": "fb443470-e3aa-4570-8b58-260c971e1e66",
  "iss": "https://auth.testxxxx.xxxx/realms/albtest",
  "aud": "account",
  "sub": "9e9a7ed9-26c3-45fc-89a3-f112072fe8c7",
  "typ": "Bearer",
  "azp": "albtest",
  "sid": "e90a823f-c13d-44b3-bd1b-f103250feff4",
  "acr": "1",
  "allowed-origins": [
    ""
  ],
  "realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization",
      "api_authentication",
      "default-roles-albtest"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid email profile",
  "email_verified": false,
  "name": "test user",
  "preferred_username": "test-user",
  "given_name": "test",
  "locale": "ja",
  "family_name": "user",
  "email": "xxxx@xxxx.com"
}
```
ユーザークレームもデコードしてみると、ユーザ情報を取得できていることが分かります。
ただし、IDトークンではないとの記載がドキュメントにありました。

## まとめ
最後に、ALB認証の仕組みが、公式リファレンス上で分かりやすく解説されていたので紹介して終わりにします。
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/3685a39b-de13-1190-6e97-c4424e03be4c.png)

|#|説明|
|:-:|:-:|
|1|ユーザーがALBの背後のWebサーバに HTTPSリクエスト。認証アクションは、リクエストヘッダー内のCookie をチェック。|
|2|Cookieが存在しない場合、ALBはユーザーをIdP認証エンドポイントにリダイレクト。|
|3|ユーザーが認証されると、IdPは認証付与コードとともにユーザーをALBに送り返す。|
|4|ALBは承認付与コードをIdPトークンエンドポイントに提示。|
|5|有効な認可付与コードを受け取ると、IdP はIDトークンとアクセストークンをALBに提供。|
|6|ALBはアクセス トークンをユーザー情報エンドポイントに送信。|
|7|ユーザー情報エンドポイントは、ユーザー要求のアクセストークンを交換。|
|8|ALBはAWSELB認証セッションCookieを持つユーザーを元のURIにリダイレクト。|
|9|ALBはCookieーを検証し、ユーザー情報をX-AMZN-OIDC-* HTTPヘッダーに設定してターゲットに転送。|
|10|ターゲットはALBに応答を返す。|
|11|ALBは最終応答をユーザーに送信。|