【keycloak on Fargate 3】ALB認証で受け取ったトークンでAPI Gateway認可も突破する

# やったこと
- https://qiita.com/yuta0003/items/b383406d09c082e252e7 にて作成したALB認証を元に、取得したアクセストークンを使用してAPI Gatewayの認可にも利用する。
- keycloak側でユーザにロールを付与しておき、そのロールがあればAPIGatewayのコールも許すよといったコードを書く
- イメージ的にはこんな感じ
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/33518817-0181-ac07-aa64-ef17262028e6.png)

# 本記事で作成するリソース
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/a2c5d38c-b742-e2e4-621e-7545f93d7361.png)

# keycloakのrealmユーザにロールを付与
「Realm roles」メニューを開き、「Create role」を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/26bfbe26-1abc-f6d2-f547-48cf8ba5a961.png)
任意のロール名を記入し、「Save」を押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/a12f1522-bc9b-2585-210c-756afc3e33b6.png)
ロールを割り当てたいユーザページに飛び、「Role mapping」タブを開いて「Assign Role」を押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/c2d496ac-169f-60a6-3883-3d93abac6958.png)

Filter選択タブを「Filter by realm roles」に変更し、先ほど作成したロールにチェックを入れて「Assign」を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/e86f7c28-c4c7-91c8-1272-617d58ae64e4.png)
Role mapping情報に追加されればOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/c5595cfa-6692-3d5e-b848-e345bbfa6b58.png)

# Lambdaオーソライザの作成
ひとまずランタイムにnodejsを選択して通常のフロー通りにLambdaを作成します。
アクセストークンの検証にライブラリ「jsonwebtoken」を使用するので、レイヤーの作成も必要となります。（手順は割愛します）

ソースコードは以下の通り。realmroleには作成したrealmロールの名前を指定します。
```javascript
import jwt from 'jsonwebtoken'
import { readFileSync } from 'fs';
import { join } from 'path';

export const handler =  function(event, context, callback) {
    const token = event.authorizationToken.replace("Bearer ", "");
    const publicKey = readFileSync(join(process.cwd(), 'public.pem'), 'utf8');
    // 作成したrealmロールの名前を指定
    const realmrole = 'api_authentication'

    // アクセストークンの検証
    jwt.verify(token, publicKey, (err, decoded) => {
        if (err) {
            // 検証に失敗した場合、API Gateway へのアクセスを拒否
            return callback('Unauthorized');
        } else {
            // トークンからロール情報を取得
            var roles = decoded.realm_access.roles
            // ロールが想定通りのものであれば対象APIメソッドのコール権限を付与
            // そうでなければアクセス拒否
            if(roles.includes(realmrole)) { 
                callback(null, generatePolicy('user', 'Allow', event.methodArn));
            } else {
                return callback('Unauthorized');
            }   
        }
    });
};

// IAMポリシーを生成する関数
const generatePolicy = (principalId, effect, resource) => {
    const policyDocument = {
        Version: '2012-10-17',
        Statement: [
            {
                Action: 'execute-api:Invoke',
                Effect: effect,
                Resource: resource,
            },
        ],
    };

    return {
        principalId,
        policyDocument,
    };
};

```
index.mjsと同じ階層にpublic.pemという名前のファイルを作成し、公開鍵を渡します。
公開鍵は、「realm settigns」メニューより、RS256アルゴリズム列の「Public key」を押下することで取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/7a35e3dd-9d14-5a62-f82c-67077dd41fde.png)
ファイルの中身は以下の通りとしてください。
```
-----BEGIN PUBLIC KEY-----
取得した公開鍵
-----END PUBLIC KEY-----
```
# API Gatewayの作成
API GatewayをRest形式で作成後し、メソッドも任意に作成します。
統合リクエストは何でもよいですが、今回は簡易検証のためMockを選びました。
レスポンスも認証が成功したと分かるようなメッセージを添えておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/4c29aff3-8742-5659-7235-35ce90a6b95f.png)
認可プロセスはメソッドリクエストで追加します。
メソッドリクエストの編集画面より、認可に作成したLambdaを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/92d63260-5cc0-9baa-4c13-790eaa5bea84.png)

# 最後にWebサーバのファイルを編集します。
サーバの構築手順は前回記事で説明済みのため割愛です。
https://qiita.com/yuta0003/items/b383406d09c082e252e7

index.phpは以下の通りに編集します。
ファイル呼び出し時に、アクセストークンを付与した状態でAPI Gatewayのメソッドを呼び出し、その応答を画面に表示するだけの簡単なプログラムを追加しました。
$urlは作成したAPI GatewayメソッドのURLを指定してください。
```html
<?php
ini_set('mbstring.internal_encoding' , 'UTF-8');

// 作成したAPI Gatewayメソッド
$url = 'https://zzzzz.execute-api.ap-northeast-1.amazonaws.com/Prd/test';
// Bearerトークン
$token = $_SERVER['HTTP_X_AMZN_OIDC_ACCESSTOKEN'];
// HTTPヘッダを設定
$headers = [
  'Authorization: Bearer ' . $token,
  'Content-Type: application/json'
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
  echo 'Error:' . curl_error($ch);
} else {
}
curl_close($ch);
?>


<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>
    <h1>Lambdaオーソライザ認証成功！！</h1>
    <h2>API認証結果</h2>
    <p><?php print_r($response)['body'] ?></p>
  </body>
</html>
```
# 動作確認
これで準備は完了です。
実際にサーバにアクセス後、以下の画面が表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/cc7d9641-5d4e-edf5-a610-7e53e97d1dd5.png)

# 最後に
keycloakxAWSで認証、認可の仕組みを整備してみました。
もちろんcognitoを使えばもっと簡単にできますが、cognitoが使えない場合の選択肢の一つとして構築経験があると安心度が増します。