ALB + Cognito認証でHTTP ヘッダーに付与されるユーザークレーム(JWT)のエンコードと署名の検証をしてみた。

2021.11.11

こんにちは、コンサル部@大阪オフィスのTodaです。

ALBとCognitoを利用して認証を有効化している場合、認証が正常にできるとALB配下のEC2にHTTPヘッダーが付与されます。
付与された情報には認証ユーザ固有の識別子やユーザ情報が含まれるのですが、 ロードバランサーから正しく送信された物かどうかなりすましでないか検証をおこなう必要がございます。
今回はユーザークレームのエンコードと署名の検証をPHPを利用しておこなってみます。

■ (参考資料) Application Load Balancer を使用してユーザーを認証するを参照してください。
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/listener-authenticate-users.html

ALB + Cognitoの認証イメージや実際の設定は下記ブログを参考ください。

前提条件

Cognitoの設定ならびにALB、EC2の設定を完了した状態から対応をいたします。
設定方法については上記ブログ「EC2とALBでコンテンツを配信している環境にCognitoで認証を追加してみました」を参考ください。
検証で利用するプログラムはPHP7.3にておこない、必要なライブラリはComposerを使用して取得します。

JWTのライブラリ選定

JWT = JSON Web Token(ジェイソン・ウェブ・トークン)はJSON形式で表記された認証情報を符号化やデジタル署名を使って安全に送受信できるように変換します。
変換された情報は見たところランダムな長い文字列になっており、認証情報を把握する事ができなくなっています。
JWTの詳細は下記ブログがわかりやすく記載されていますのでご確認下さい。

今回はJWTを検証するためJWT用のライブラリを選定して利用します。
ライブラリはプログラム単位で分かれていて数も多くあります。
ライブラリの種類は下記サイトにまとめられていますので参照をおこない利用するライブラリを選びます。

■ JWT Libraries for Token Signing/Verification
https://jwt.io/libraries

AWSの公式に掲載されているJSONWebトークンの検証を参照したところRS256暗号化アルゴリズムの使用が記載ございましたので「RS256」に対応ができ、「PHP7.3」で動作するライブラリということで「firebase/php-jwt」を利用します。

■ JSONWebトークンの検証
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html

テスト用のプログラム構築

HTTPヘッダーの内容確認

検証をおこなうためALB配下のEC2のWebサーバにindex.phpを配置して認証後にアクセスするように設定します。
試しにHTTPヘッダーに情報が付与されているかを確認します。

HTTPヘッダー付与内容

  • HTTP_X_AMZN_OIDC_ACCESSTOKEN:アクセストークン
  • HTTP_X_AMZN_OIDC_IDENTITY:ユーザーの一意の識別子 (UUID)
  • HTTP_X_AMZN_OIDC_DATA:ユーザークレーム (JWT)
<?php
    echo("■ HTTP_X_AMZN_OIDC_ACCESSTOKEN <br />");
    print_r($_SERVER['HTTP_X_AMZN_OIDC_ACCESSTOKEN']);
    echo("<br /><br />");
    echo("■ HTTP_X_AMZN_OIDC_IDENTITY <br />");
    print_r($_SERVER['HTTP_X_AMZN_OIDC_IDENTITY']);
    echo("<br /><br />");
    echo("■ HTTP_X_AMZN_OIDC_DATA <br />");
    print_r($_SERVER['HTTP_X_AMZN_OIDC_DATA']);

出力イメージ
HTTPヘッダーの内容確認

JWTライブラリを利用した検証

firebase/php-jwtを利用するためComposerを利用してライブラリの取得をおこないます。

composer require firebase/php-jwt

実際のプログラムを構築していきます。
ユーザークレームの検証をおこなう際に公開鍵の取得が必要です。
公開鍵は Cognito API利用による認証と ALB + Cognitoの認証で変わるようです。

証明書URL: https://public-keys.auth.elb.【region】.amazonaws.com/【key-id】
※ URLの【】の項目は置きかえが必要です。
【region】:AWSリージョン指定
【key-id】:HTTP_X_AMZN_OIDC_DATA内を分解し際にあるkidを指定

<?php
    error_reporting(E_ALL);

    require_once "vendor/autoload.php";

    try {
        $region  = 'ap-northeast-1';           # リージョン指定
        $pool_id = '[CognitoプールID指定]';      # CognitoプールID
        $alg     = 'ES256';                    # 暗号化指定 (ES256固定)
        $cognito_idp_url = 'https://cognito-idp.'. $region .'.amazonaws.com/'. $pool_id;

        # HEADER HTTP_X_AMZN_OIDC_DATA の内容分解
        $encoded_jwt = $_SERVER['HTTP_X_AMZN_OIDC_DATA'];
        $jwt_headers = explode('.', $encoded_jwt);
        if (count($jwt_headers) != 3) {
            throw new Exception('JWTフォーマット取得 失敗');
        }
        list($jwt_headb64, $jwt_bodyb64, $jwt_cryptob64) = $jwt_headers;

        $jwt_headb = base64_decode($jwt_headb64);
        $decoded_json = json_decode($jwt_headb);
        $kid = $decoded_json->kid;

        # 公開鍵取得
        $url = 'https://public-keys.auth.elb.' . $region . '.amazonaws.com/' . $kid;
        $pub_key = file_get_contents($url);

        # JWT Decoded
        $result = Firebase\JWT\JWT::decode($encoded_jwt, $pub_key, [$alg]);

        echo("JWT検証OK<br />");
        print_r($result);

    }catch(Exception $e){
        print_r($e->getMessage());
    }

?>
<br />-------------------------------------<br />
<?php
    echo("■ HTTP_X_AMZN_OIDC_ACCESSTOKEN <br />");
    print_r($_SERVER['HTTP_X_AMZN_OIDC_ACCESSTOKEN']);
    echo("<br /><br />");
    echo("■ HTTP_X_AMZN_OIDC_IDENTITY <br />");
    print_r($_SERVER['HTTP_X_AMZN_OIDC_IDENTITY']);
    echo("<br /><br />");
    echo("■ HTTP_X_AMZN_OIDC_DATA <br />");
    print_r($_SERVER['HTTP_X_AMZN_OIDC_DATA']);
?>
<br />-------------------------------------<br />

正常に処理ができるとユーザの情報を参照することができます。

出力イメージ JWTライブラリを利用した検証

さいごに

今回はALB + Cognito認証でHTTP ヘッダーに付与されるユーザークレーム(JWT)のエンコードと署名の検証を試してみました。
少しでもお客様の参考になればと考えております。