Amazon Cognitoで構築するスケーラブルなWebアプリケーション③ - JWTからユーザー情報を取得する

記事タイトルとURLをコピーする

こんにちは。
アプリケーションサービス部、DevOps担当の兼安です。

本記事はこちらの記事の続きです。

blog.serverworks.co.jp

今回は、バックエンドプログラムでJWT(JSON Web Token)を解析し、ユーザー情報を取得する方法を説明します。

本記事のターゲット

Webアプリケーションの開発経験があり、今後、Webアプリケーションをスケーラブルにしたい方を対象としています。
記事中にロードバランサー(=ALB)やAmazon EC2などが出てきますが、これらの説明は割愛していますので、AWSのコンピューティングサービスやデータベースサービスの知識があることが前提となります。

今回の題材

Amazon Cognitoで認証すると、HTTPリクエストのヘッダーにJWT(JSON Web Token)が付与されます。
このJWTには、Amazon Cognitoで管理しているユーザー情報が含まれており、プログラムでこのJWTを解析することで、ユーザー情報を取得することができます。
JWTからユーザー情報を取得することで、Webアプリケーションのバックエンドプログラムで、ユーザー情報を利用した処理を行うことができます。
今回は、このJWTからユーザー情報を取得する方法を説明します。

JWT(JSON Web Token)とは

datatracker.ietf.org

JWT(JSON Web Token)は、JSON形式を用いて二者間で安全に情報をやり取りするためのオープンスタンダードです。
主な用途は、認証情報のやり取りです。

Amazon CognitoにおけるJWT

docs.aws.amazon.com

こちらのページにある通り、Amazon CognitoはOpenID Connect(OIDC)仕様に基づいたJWTを発行し、認証情報をやり取りします。

Amazon Cognito は、OpenID Connect (OIDC) 仕様の整合性と機密性の一部を使用するトークンを発行します。

署名を比較することで、JWTの整合性を確認することができます。

悪意のあるユーザーがトークンを変更することがありますが、アプリケーションがパブリックキーを取得して署名を比較すれば、トークンは一致しなくなります。

JWTは、以下の3つの部分から構成されます。

項目 説明
ヘッダ トークンの種類や署名アルゴリズムなどのメタデータ
ペイロード トークンに含まれる情報
署名 トークンの整合性を確認するための署名

バックエンドプログラムでJWTからユーザー情報を取得する際には、ペイロードの情報を利用します。

JWTとOpenID Connect(OIDC)

AWS Cognitoのページを読んでいくと、OpenID Connect(OIDC)という言葉が出てきますので、こちらも説明しておきます。

openid.net

OpenID Connect(以降、OIDCと略します)は、OAuth 2.0プロトコルをベースにした認証プロトコルです。
OAuthは認可を行うプロトコルであり、それに認証のプロセスを追加したものがOIDCです。
認証と認可の違いは前回の記事を参照してください。
OAuthの提供する認可の仕組みでは賄いきれない領域があり、それを補完するためにOAuthを拡張したのがOIDCです。
この辺りの詳細な話は、今回は割愛させていただきます。

OAuthは、認可の仕組みとしてアクセストークンとリフレッシュトークンを提供します。
OIDCは、OAuthに認証の仕組みを追加し、OAuthが提供するトークンに加えてIDトークンを提供します。

OIDCにおいて、トークンに用いられるのがJWTです。
本記事で述べているJWTからユーザー情報を取得するというのは、OIDCにおけるIDトークンからユーザー情報を取得するという意味になります。

なお、Amazon Cognitoからユーザー情報を取得する方法には、ユーザー属性エンドポイントを利用する方法もあります。
こちらは別の機会で触れるとして、本記事では対象外としています。

docs.aws.amazon.com

Amazon Cognitoが発行するJWTの検証

Amazon Cognitoが発行するJWTの検証は、以下の手順で行います。

  • HTTPリクエストのヘッダーからJWTを取得
  • JWTのヘッダー部分をデコードして、kid(Key ID)を取得
  • kidを使って、公開鍵を取得
  • JWTのデコードと公開鍵を使った署名検証

詳細は以下のページを参照してください。

docs.aws.amazon.com

github.com

ALBを使用した場合の公開鍵の取得方法

この後サンプルプログラムを使ってJWTからユーザー情報を取得する方法を説明します。
本記事のサンプルプログラムは、前回に引き続き、ALBを使用した構成を想定しています。

構成図

ALBを使用した場合、公開鍵の取得URLは以下のようになります。

https://public-keys.auth.elb.region.amazonaws.com/key-id

ALBを使わない場合の公開鍵の取得URLは、上述のAWS公式ページを参照してください。

JWTからユーザー情報を取得するサンプルプログラム

以下のサンプルプログラムは、JWTからユーザー情報を取得するためのものです。
このサンプルプログラムは、PHPとLaravelを使用しています。
このサンプルコードgetCognitoPayloadは、JWTを解析してペイロードを取得するメソッドです。
ペイロードを取得することで、ユーザー情報を取得することができます。

下記サンプルプログラムでは、ペイロードをそのままレスポンスに返していますが、実際の運用では、このメソッドのreturnの部分を修正し、ペイロードをそのまま返して、ユーザー情報をどう抜き取るかは呼び出し側に任せるイメージです。

<?php

namespace App\Http\Controllers;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use GuzzleHttp\Client;
use Illuminate\Http\Request;

require_once __DIR__ . '/../../../vendor/autoload.php';

abstract class Controller
{
    /**
     * リクエストヘッダーからJWTを取得して解析するメソッド
     * コントローラーの基底クラスに実装して、他のコントローラーで継承して利用する想定
     * @param Request $request HTTPリクエスト
     * @see https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/listener-authenticate-users.html
     * */
    protected function getCognitoPayload(Request $request)
    {
        // Step 1: ALB から送られる JWT を取得
        $encoded_jwt = $request->header('x-amzn-oidc-data');

        if (!$encoded_jwt) {
            return response()->json(['error' => 'No JWT found in headers'], 400);
        }

        // Step 2: JWT のヘッダー部分をデコードして "kid" を取得
        $jwt_headers = explode('.', $encoded_jwt);
        $jwt_head = json_decode(base64_decode(strtr($jwt_headers[0], '-_', '+/')), true);

        if (!isset($jwt_head['kid'])) {
            return response()->json(['error' => '"kid" not found in JWT headers'], 400);
        }

        $kid = $jwt_head['kid'];
        $region = 'ap-northeast-1'; // ALB のリージョン

        // Step 3: ALB の公開鍵を取得
        $url = "https://public-keys.auth.elb.$region.amazonaws.com/$kid";
        $client = new Client();
        try {
            $response = $client->get($url);
            $pub_key = $response->getBody()->getContents();
        } catch (\Exception $e) {
            return response()->json(['error' => 'Failed to fetch public key', 'message' => $e->getMessage()], 500);
        }

        // Step 4: JWTのデコードと公開鍵を使った署名検証
        try {
            $decoded_payload = JWT::decode($encoded_jwt, new Key($pub_key, 'ES256'));
            // テストのため、取得したペイロードをそのままレスポンスに返しています
            // 実際の運用では、ペイロードをそのまま返して、ユーザー情報をどう抜き取るかは呼び出し側に任せるイメージです
            return response()->json($decoded_payload, 200, [], JSON_PRETTY_PRINT);
        } catch (\Exception $e) {
            return response()->json(['error' => 'JWT verification failed', 'message' => $e->getMessage()], 400);
        }
    }
}

このプログラムで取得できるペイロードは、以下のような内容になります。

{
    "sub": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "email": "satoshi.kaneyasu@serverworks.co.jp",
    "username": "kaneyasu",
    "exp": 1676316377,
    "iss": "https://cognito-idp.<Region>.amazonaws.com/<your user pool ID>"
}
項目 説明
sub 認証されたユーザーの固有識別子 (UUID) またはサブジェクト
email メールアドレス
username ユーザー名
exp ユーザーのトークンの有効期限が切れる有効期限 (Unix の時間形式)
iss トークンを発行した ID プロバイダーのURL

emailusernameなどは、Amazon Cognitoでユーザープールに登録した属性情報です。

Amazon Cognitoでユーザープールに登録した属性情報

ユーザープールの属性情報は可変なので、必要に応じて追加・変更することができます。
増やした属性情報もペイロードに含めることができます。

従って、アプリケーションが権限制御に使用する情報を属性情報に追加してペイロードに含めるようにすれば、プログラム側はサインインしたユーザーに応じた処理に繋げることができます。

アプリケーションのデータベースとの連携

Amazon Cognitoが付与するJWTからユーザー情報を取得できます。
一方で、ユーザー情報はAmazon Cognitoのユーザープールだけに保存しておけばよいのでしょうか?
アプリケーションの一般的な実装では、ユーザー情報と他のデータを結合して利用します。
SQLで言うところのJOINですね。
私は結合をしやすくするためには、アプリケーションのデータベースにもユーザー情報が保存されていた方が便利だと思っています。
Amazon Cognitoを導入したからといって、データの結合方法まで変えるのは本意ではありません。

アプリケーションのデータベースにもユーザー情報が保存するとしたら、以下のようなテーブル定義になるかと思います。
(テーブルはPostgreSQLを想定しています)

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- アプリ内のユーザーID(内部管理用)
  cognito_sub UUID NOT NULL UNIQUE, -- Cognitoのユーザー識別子(sub)
  email VARCHAR(255) NOT NULL UNIQUE, -- メールアドレス
  username VARCHAR(100) NOT NULL, -- ユーザー名
  first_name VARCHAR(100), -- 名(オプション)
  last_name VARCHAR(100), -- 姓(オプション)
  phone_number VARCHAR(20) UNIQUE, -- 電話番号(オプション)
  user_class VARCHAR(50) DEFAULT 'GUEST', -- ユーザークラス(GUEST, ADMINなど)
  user_status VARCHAR(50) DEFAULT 'ACTIVE', -- ユーザーステータス(ACTIVE, DISABLEDなど)
  created_at TIMESTAMPTZ DEFAULT now(), -- 作成日時
  updated_at TIMESTAMPTZ DEFAULT now() -- 更新日時
);

cognito_subは、JWTのペイロードに含まれるsubの値を保存するカラムです。
SQLで任意のデータとユーザー情報を結合する場合、以下の条件をつけるイメージです。

SELECT
    example_table.*,
    users.id,
    users.username,
    users.email
FROM users
INNER JOIN example_table
ON users.id = example_table.user_id
WHERE users.cognito_sub = 'JWTのペイロードから取得したsub';

次回の内容

今回は、プログラムでJWT(JSON Web Token)を解析し、ユーザー情報を取得する方法を説明しました。
後半では、アプリケーションのデータベースにもユーザー情報を保存する方式について説明しましたが、この方式はいわばユーザー情報の二重管理です。
アプリケーションのデータベースとAmazon Cognitoでユーザー情報を同期させる実装方法を解説します。

blog.serverworks.co.jp

兼安 聡(執筆記事の一覧)

アプリケーションサービス部 DS3課所属
2025 Japan AWS Top Engineers (AI/ML Data Engineer)
2025 Japan AWS All Certifications Engineers
2025 AWS Community Builders
Certified ScrumMaster
PMP
広島在住です。今日も明日も修行中です。
X(旧Twitter)