Amazon Cognito オーソライザーによる Amazon API Gatewayへのアクセス制御

Amazon Cognito オーソライザーによる Amazon API Gatewayへのアクセス制御

目次

    はじめに

    クラウドインテグレーション部、
    クラウドソリューション2課の川井です。

    APIのセキュリティ対策として認証・認可の仕組みを導入することは、重要視されています。
    特に、サーバーレス環境やマイクロサービスアーキテクチャにおいては、適切な認証・認可を実装することで、
    より安全なAPI運用が可能になります。

    そこで本記事では、Amazon API Gateway(以下、API Gateway)と AWS Lambda(以下、Lambda)を組み合わせたAPIに対して、Amazon Cognito(以下、Cognito)をオーソライザーとして活用し、アクセス制御を行う方法を実装します。
    これにより、APIへのアクセスを認証済みユーザーのみに制限し、安全なAPI運用を実現できます。

    また、Cognitoから IDトークンを取得する方法については、Curlコマンドを使用して、クライアントシークレットなしでトークンを取得する方法と、
    クライアントシークレットを使用して、SECRET_HASHを計算する方法の、2つのアプローチを実施します。

    なお、Cognitoには OAuth2.0を使用した認証方法もありますが、
    本記事ではその方法については記載しておりませんので、ご了承ください。


    構成概要

    Cognitoをオーソライザーとして設定し、API Gateway経由でLambda関数に、
    アクセスする流れは以下のようになります。
    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_01

    ① クライアント → Cognito : ユーザー認証リクエスト

    クライアントが、Cognito User Poolに対して ユーザー認証リクエストを送信します。
    認証方法は、ユーザー名 + パスワードを使用していきます。

    ② Cognito → クライアント : IDトークン発行

    認証に成功すると、Cognitoは IDトークン、アクセス トークン、リフレッシュトークンをクライアントに返します。 このうち、IDトークンをAPI Gatewayのオーソライザーで使用します。

    ③ クライアント → API Gateway : IDトークンを付けてAPIリクエスト

    クライアントは、Cognitoから取得したIDトークンを、リクエストの際、Authorizationヘッダーに設定し、API Gatewayに送信します。

    リクエストの例(IDトークン付き)

    GET /protected-resource HTTP/1.1
    Host: api.example.com
    Authorization: Bearer <IDトークン>

    ④ API Gateway → Cognito : トークンの検証

    API Gatewayは、オーソライザーとして設定された Cognito User Poolに IDトークンを送信し、有効性を検証します。
    具体的には、トークンの署名検証、発行元(Issuer)の確認、有効期限の確認などが行われます。

    ⑤ Cognito → API Gateway : トークンの判定

    トークンが有効な場合 → API GatewayはリクエストをLambdaに転送します。
    トークンが無効な場合 → API Gatewayは「401 Unauthorized」エラーをクライアントに返します。

    ⑥ API Gateway → Lambda : ユーザー情報付きでリクエスト転送

    Cognitoで認証された場合、API Gatewayはユーザー情報(sub、email、cognito:groups など)を含めてLambdaにリクエストを送信します。

    ⑦ Lambda → API Gateway : APIレスポンスを返す

    Lambdaはリクエストを処理し、結果を API Gatewayに返します。

    ⑧ API Gateway → クライアント : HTTPレスポンスを返す

    API Gatewayは、Lambdaからのレスポンスを受け取り、クライアントに返します。


    このフローにより、Cognitoによる認証を通過したユーザーのみが APIにアクセスできるようになり、安全なアクセス制御が実現できます。


    また、トークンを保持していない場合、API Gatewayは「401 Unauthorized」エラーを返し、クライアントのアクセスは拒否されます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_02

    Cognito ユーザープール作成

    ※API GatewayやLambdaは既に作成済みのものとして進めていきます。
    そのため、これらのリソースの作成は省略していますので、ご了承ください。

    現在、API Gatewayのメソッドリクエストには、認証やアクセス制限を設定していないため、
    Lambdaから {"message": "Hello from Lambda!"} というレスポンスがそのまま返されています。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_03

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_04

    こちらを、IDトークンが付与されたリクエストのみがアクセスできるように設定していきたいと思います。


    では、認証基盤となるCognitoユーザープールを作成します。
    ユーザープール作成画面では、アプリケーションクライアントのタイプを選択するためのオプションが表示されています。
    こちらは、Cognitoユーザープールを作成した後に追加・削除が容易にできます。

    まずは、クライアントシークレットを使わず、認証処理を完結させますので、
    「シングルページアプリケーション(SPA)」を選択します。アプリケーション名には任意の名前の「AppClient」を入力します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_05

    各選択肢の使い分けは、以下のようになるかと思います。

    ・ クライアントシークレットを使用して認証させる場合は「従来のウェブアプリケーション」
    ・ リフレッシュトークンを活用して、ログインを維持させるなどの要件がある場合は「モバイルアプリ」
    ・ OAuth2.0を使用した認証を使用する場合は「Machine to Machine(M2M)アプリケーション」


    続いて、オプション設定では、 「サインイン識別子のオプション」と「サインアップのための必須属性」を選択します。
    今回は、ユーザープールのログイン画面は使用しませんが、ログイン画面で入力する項目の選択オプションになります。

    サインイン識別子は「ユーザー名」にし、ユーザー名サインインでは、サインアップのための必須属性に
    メールアドレスまたは電話番号を必須属性として選択する必要がありますので、「email」を選択してユーザープールを作成していきます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_06

    まず、ユーザープールの名前を変更します。コンソール画面からのユーザープール作成画面では、ユーザープール名を指定できないため、概要ページの名前変更から「cognito-test-userpool」という名前にユーザープール名を変更します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_07

    次に、作成したアプリケーションクライアントを選択して、編集をクリックします。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_08

    認証方法は、ユーザー名 + パスワードを使用していきますので「ユーザー名とパスワード (ALLOW_USER_PASSWORD_AUTH) を使用してサインインします」に、チェックを入れて保存します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_09

    そして、次に認証で使用するユーザーを作成します。ユーザー作成ページに移動して「ユーザーを作成」をクリックします。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_10

    ユーザー名を「cognito-test-user」、Eメールアドレスとパスワードを任意の値で入力して、ユーザーを作成します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_11

    ユーザーが作成されたことを確認できたら、続いてパスワードを変更します。
    パスワードポリシーの設定によって異なりますが、デフォルトでは自動的に7日後に変更されてしまいますので変更していきます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_12

    今回は以下のAWS CLIコマンドでユーザーパスワードを変更します。

    aws cognito-idp admin-set-user-password \
    --region ap-northeast-1 \
    --user-pool-id <User Pool ID> \
    --username <ユーザー名> \
    --password <パスワード> \
    --permanent

    CognitoのユーザープールのIDは、Cognitoコンソール画面の概要ページから確認できます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_13

    AWSのCloudShellを使用してコマンドを実行します。エラーなどが返ってこなければ成功です。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_14

    Cognitoコンソール画面のユーザーページを確認して、無事パスワードが変更されていれば、確認ステータスが「確認済み」に変更されます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_15

    これで、Cognitoの設定は完了です。続いて、API Gateway側でCognitoオーソライザーの設定を行っていきます。


    API Gateway オーソライザー設定

    ※先程も記載していますが、API GatewayやLambdaは既に作成済みのものとして進めていきます。
    そのため、これらのリソースの作成は省略していますので、ご了承ください。

    API Gatewayのコンソール画面から、作成済みのAPI Gatewayを選択し、「オーソライザー」を開いて「オーソライザーの作成」をクリックします。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_16

    オーソライザー名には任意の名前「cognito-test-authorizer」を入力します。
    そして、オーソライザーのタイプには「Cognito」を選択して、先程作成したユーザープールの「cognito-test-userpool」を選択します。
    トークンのソースは「Authorization(※これが一般的)」として、オーソライザーを作成します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_17

    作成されたオーソライザーのテストができるので、実際にIDトークンを取得してテストしてみます。
    IDトークン取得のため、以下のCurlコマンドを実行します。ClientIdとUSERNAME、PASSWORDは設定されている値を入力してください。

    curl -X POST https://cognito-idp.ap-northeast-1.amazonaws.com \
    -H "Content-Type: application/x-amz-json-1.1" \
    -H "X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth" \
    -d '{
    "AuthFlow": "USER_PASSWORD_AUTH",
    "ClientId": "<アプリケーションクライアントのID>",
    "AuthParameters": {
    "USERNAME": " <ユーザー名>",
    "PASSWORD": "<ユーザーパスワード>"
    }
    }' | jq

    アプリケーションクライアントのIDは、Cognitoコンソール画面のアプリケーションクライアントの画面に記載されています。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_18


    Curlコマンドが問題なく実行されれば、AccessToken、IdToken、RefreshTokenが取得できたことを確認できます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_19

    そして、取得したトークンの中のIDトークンを使用してオーソライザーテストを実行してみると、200のレスポンスが返ってきました。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_20

    オーソライザーのテストが正常に完了したため、次にAPI Gatewayのメソッドにオーソライザーを設定します。
    今回は、/test/status のGETメソッドに適用していきます。メソッドリクエストの設定で「編集」をクリックします。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_21

    認可の設定では、先程作成したオーソライザーの「cognito-test-authorizer」を選択し、その他の設定は変更せず、そのまま保存します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_22

    オーソライザーの設定が完了しましたら、「APIのデプロイ」からステージのデプロイ実行をします。これを行わないと設定が反映されません。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_23


    APIへのアクセス(クライアントシークレットなし)

    では、APIへリクエストを送信していきます。

    まずは、ID トークンなしでリクエストを送信したところ、{"message":"Unauthorized"} というレスポンスが返されました。
    オーソライザーを設定する前は {"message": "Hello from Lambda!"} が返っていましたが、設定後は認証なしのリクエストが拒否されるようになりました。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_24

    続いては、ID トークンを付与し、-H "Authorization: Bearer $TOKEN" をリクエストに追加して送信したところ、{"message": "Hello from Lambda!"} というレスポンスが返されました。
    これにより、Cognitoオーソライザーの設定が正常に動作していることが確認できます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_25


    これで、APIへのアクセスを認証済みユーザーのみに制限し、安全なAPI運用を実現させることができました。
    続いては、クライアントシークレットを使用して、Cognito認証を実行していきます。


    APIへのアクセス(クライアントシークレットあり)

    クライアントシークレットを使用して、Cognitoからトークンを取得するには、クライアントシークレットをもとに
    SECRET_HASH を計算し、計算したSECRET_HASHをCurlコマンドに追加する必要があります。


    まずは、Cognitoコンソール画面から、新たにアプリケーションクライアントを作成します。作成するアプリケーションクライアントは、クライアントシークレットを使用して認証させるため「従来のウェブアプリケーション」になります。
    アプリケーション名には任意の名前で「SecureAppClient」と入力してアプリケーションクライアントを作成します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_26

    作成しましたら、先程と同様で認証方法は、ユーザー名 + パスワードを使用していきますので
    「ユーザー名とパスワード (ALLOW_USER_PASSWORD_AUTH) を使用してサインインします」にチェックを入れて保存します。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_27

    前回作成したアプリケーションクライアントとは異なり、今回作成したものには「クライアントシークレット」の項目が追加されています。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_28


    現状、このアプリケーションクライアントでCurlコマンドを実行すると、
    「Client <アプリケーションクライアントのID> is configured with secret but SECRET_HASH was not received」というエラー表示が返ってきます。
    つまり、SECRET_HASHがないがためにトークンが取得できないということです。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_29

    では、クライアントシークレットをもとにSECRET_HASHを計算して、CurlコマンドにSECRET_HASHを追加していきます。
    以下の公式ドキュメントに記載されているSECRET_HASHの計算コードを参考に使用して、SECRET_HASHを計算します。
    AWS公式ドキュメント

    SECRET_HASHの計算はLambdaで実行しますので、PythonのLambda関数を作成します。
    そして、以下のコードを実行します。username、app_client_id、client_secretの値は設定されている値を入力してください。

    usernameは作成したユーザー名を入力、
    app_client_id、client_secretは、作成したアプリケーションクライアントの画面で両方確認できます。

    import hmac
    import hashlib
    import base64

    def calculate_secret_hash(username, app_client_id, client_secret):
    """ HMAC-SHA256 を使って SECRET_HASH を計算 """
    message = (username + app_client_id).encode("utf-8")
    key = client_secret.encode("utf-8")
    return base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()

    def lambda_handler(event, context):
    """ AWS Lambda のエントリーポイント (ハンドラ関数) """

    # 直接変数を定義(環境変数を使うことも可能)
    username = "<ユーザー名>"
    app_client_id = "<アプリケーションクライアントのID>"
    client_secret = "<クライアントシークレットの値>"

    # SECRET_HASH を計算
    secret_hash = calculate_secret_hash(username, app_client_id, client_secret)

    # 結果を JSON 形式で返す
    return {
    "statusCode": 200,
    "body": {
    "SECRET_HASH": secret_hash
    }
    }

    Lambda関数が問題なく実行されれば、以下画像のように、クライアントシークレットをもとに計算された
    SECRET_HASHの値が返ってきます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_30

    SECRET_HASHの値を追加して、以下のCurlコマンドを実行します。

    curl -X POST https://cognito-idp.ap-northeast-1.amazonaws.com \
    -H "Content-Type: application/x-amz-json-1.1" \
    -H "X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth" \
    -d '{
    "AuthFlow": "USER_PASSWORD_AUTH",
    "ClientId": "<アプリケーションクライアントのID>",
    "AuthParameters": {
    "USERNAME": " <ユーザー名>",
    "PASSWORD": "<ユーザーパスワード>",
    "SECRET_HASH": "<SECRET_HASH値>"
    }
    }' | jq

    Curlコマンドが問題なく実行されれば、AccessToken、IdToken、RefreshTokenが取得できたことを確認できます。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_31


    では、APIへリクエストを送信していきます。

    ID トークンを付与し、-H "Authorization: Bearer $TOKEN" をリクエストに追加して送信したところ、{"message": "Hello from Lambda!"} というレスポンスが返されました。
    クライアントシークレットありで、Cognitoオーソライザーの設定が正常に動作していることが確認できました。

    2503_Amazon Cognito Authorizer to control access to Amazon API Gateway_32


    以上で、CognitoからIDトークンを取得し、Curlコマンドを使用して クライアントシークレットなしと
    クライアントシークレットを使用してSECRET_HASHを計算する方法の2つのアプローチで、APIへのアクセスを実装しました。


    まとめ

    本記事では、CognitoをオーソライザーとしてAPI Gatewayに適用し、認証付きAPIへのアクセスを説明しました。
    また、以下2つのアプローチ方法を用いて、CognitoからのIDトークン取得とAPIへのアクセスを実装しました。

    1️⃣ クライアントシークレットなしでIDトークンを取得
    2️⃣ クライアントシークレットを使用し、SECRET_HASHを計算してIDトークンを取得

    クライアントシークレットなしの方法は、シンプルなフロントエンド認証向け、SECRET_HASHを計算する方法はセキュリティを強化しつつ、APIを保護する用途に適していることが分かりました。

    Cognitoを活用することで、APIの認証処理をサーバー側で実装せずに、シンプルに管理できる点は非常に便利だと感じました。
    特に、API Gatewayにオーソライザーを設定するだけで、認証の仕組みを簡単に適用できるのは大きなメリットです。
    一方で、クライアントシークレットを使用する場合には、SECRET_HASH の計算が必要になるため、実装時にはその仕組みを理解することが重要でした。

    では、また別の記事を執筆しますので、引き続きよろしくお願いいたします。
    ありがとうございました。


    クラウドインテグレーション部
    クラウドソリューション2課
    川井 康敬

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。