クラウドインテグレーション部、
クラウドソリューション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関数に、
アクセスする流れは以下のようになります。
クライアントが、Cognito User Poolに対して ユーザー認証リクエストを送信します。
認証方法は、ユーザー名 + パスワードを使用していきます。
認証に成功すると、Cognitoは IDトークン、アクセス トークン、リフレッシュトークンをクライアントに返します。 このうち、IDトークンをAPI Gatewayのオーソライザーで使用します。
クライアントは、Cognitoから取得したIDトークンを、リクエストの際、Authorizationヘッダーに設定し、API Gatewayに送信します。
リクエストの例(IDトークン付き)
GET /protected-resource HTTP/1.1
Host: api.example.com
Authorization: Bearer <IDトークン>
API Gatewayは、オーソライザーとして設定された Cognito User Poolに IDトークンを送信し、有効性を検証します。
具体的には、トークンの署名検証、発行元(Issuer)の確認、有効期限の確認などが行われます。
トークンが有効な場合 → API GatewayはリクエストをLambdaに転送します。
トークンが無効な場合 → API Gatewayは「401 Unauthorized」エラーをクライアントに返します。
Cognitoで認証された場合、API Gatewayはユーザー情報(sub、email、cognito:groups など)を含めてLambdaにリクエストを送信します。
Lambdaはリクエストを処理し、結果を API Gatewayに返します。
API Gatewayは、Lambdaからのレスポンスを受け取り、クライアントに返します。
このフローにより、Cognitoによる認証を通過したユーザーのみが APIにアクセスできるようになり、安全なアクセス制御が実現できます。
また、トークンを保持していない場合、API Gatewayは「401 Unauthorized」エラーを返し、クライアントのアクセスは拒否されます。
※API GatewayやLambdaは既に作成済みのものとして進めていきます。
そのため、これらのリソースの作成は省略していますので、ご了承ください。
現在、API Gatewayのメソッドリクエストには、認証やアクセス制限を設定していないため、
Lambdaから {"message": "Hello from Lambda!"} というレスポンスがそのまま返されています。
こちらを、IDトークンが付与されたリクエストのみがアクセスできるように設定していきたいと思います。
では、認証基盤となるCognitoユーザープールを作成します。
ユーザープール作成画面では、アプリケーションクライアントのタイプを選択するためのオプションが表示されています。
こちらは、Cognitoユーザープールを作成した後に追加・削除が容易にできます。
まずは、クライアントシークレットを使わず、認証処理を完結させますので、
「シングルページアプリケーション(SPA)」を選択します。アプリケーション名には任意の名前の「AppClient」を入力します。
各選択肢の使い分けは、以下のようになるかと思います。
・ クライアントシークレットを使用して認証させる場合は「従来のウェブアプリケーション」
・ リフレッシュトークンを活用して、ログインを維持させるなどの要件がある場合は「モバイルアプリ」
・ OAuth2.0を使用した認証を使用する場合は「Machine to Machine(M2M)アプリケーション」
続いて、オプション設定では、 「サインイン識別子のオプション」と「サインアップのための必須属性」を選択します。
今回は、ユーザープールのログイン画面は使用しませんが、ログイン画面で入力する項目の選択オプションになります。
サインイン識別子は「ユーザー名」にし、ユーザー名サインインでは、サインアップのための必須属性に
メールアドレスまたは電話番号を必須属性として選択する必要がありますので、「email」を選択してユーザープールを作成していきます。
まず、ユーザープールの名前を変更します。コンソール画面からのユーザープール作成画面では、ユーザープール名を指定できないため、概要ページの名前変更から「cognito-test-userpool」という名前にユーザープール名を変更します。
次に、作成したアプリケーションクライアントを選択して、編集をクリックします。
認証方法は、ユーザー名 + パスワードを使用していきますので「ユーザー名とパスワード (ALLOW_USER_PASSWORD_AUTH) を使用してサインインします」に、チェックを入れて保存します。
そして、次に認証で使用するユーザーを作成します。ユーザー作成ページに移動して「ユーザーを作成」をクリックします。
ユーザー名を「cognito-test-user」、Eメールアドレスとパスワードを任意の値で入力して、ユーザーを作成します。
ユーザーが作成されたことを確認できたら、続いてパスワードを変更します。
パスワードポリシーの設定によって異なりますが、デフォルトでは自動的に7日後に変更されてしまいますので変更していきます。
今回は以下の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コンソール画面の概要ページから確認できます。
AWSのCloudShellを使用してコマンドを実行します。エラーなどが返ってこなければ成功です。
Cognitoコンソール画面のユーザーページを確認して、無事パスワードが変更されていれば、確認ステータスが「確認済み」に変更されます。
これで、Cognitoの設定は完了です。続いて、API Gateway側でCognitoオーソライザーの設定を行っていきます。
※先程も記載していますが、API GatewayやLambdaは既に作成済みのものとして進めていきます。
そのため、これらのリソースの作成は省略していますので、ご了承ください。
API Gatewayのコンソール画面から、作成済みのAPI Gatewayを選択し、「オーソライザー」を開いて「オーソライザーの作成」をクリックします。
オーソライザー名には任意の名前「cognito-test-authorizer」を入力します。
そして、オーソライザーのタイプには「Cognito」を選択して、先程作成したユーザープールの「cognito-test-userpool」を選択します。
トークンのソースは「Authorization(※これが一般的)」として、オーソライザーを作成します。
作成されたオーソライザーのテストができるので、実際に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コンソール画面のアプリケーションクライアントの画面に記載されています。
Curlコマンドが問題なく実行されれば、AccessToken、IdToken、RefreshTokenが取得できたことを確認できます。
そして、取得したトークンの中のIDトークンを使用してオーソライザーテストを実行してみると、200のレスポンスが返ってきました。
オーソライザーのテストが正常に完了したため、次にAPI Gatewayのメソッドにオーソライザーを設定します。
今回は、/test/status のGETメソッドに適用していきます。メソッドリクエストの設定で「編集」をクリックします。
認可の設定では、先程作成したオーソライザーの「cognito-test-authorizer」を選択し、その他の設定は変更せず、そのまま保存します。
オーソライザーの設定が完了しましたら、「APIのデプロイ」からステージのデプロイ実行をします。これを行わないと設定が反映されません。
では、APIへリクエストを送信していきます。
まずは、ID トークンなしでリクエストを送信したところ、{"message":"Unauthorized"} というレスポンスが返されました。
オーソライザーを設定する前は {"message": "Hello from Lambda!"} が返っていましたが、設定後は認証なしのリクエストが拒否されるようになりました。
続いては、ID トークンを付与し、-H "Authorization: Bearer $TOKEN" をリクエストに追加して送信したところ、{"message": "Hello from Lambda!"} というレスポンスが返されました。
これにより、Cognitoオーソライザーの設定が正常に動作していることが確認できます。
これで、APIへのアクセスを認証済みユーザーのみに制限し、安全なAPI運用を実現させることができました。
続いては、クライアントシークレットを使用して、Cognito認証を実行していきます。
クライアントシークレットを使用して、Cognitoからトークンを取得するには、クライアントシークレットをもとに
SECRET_HASH を計算し、計算したSECRET_HASHをCurlコマンドに追加する必要があります。
まずは、Cognitoコンソール画面から、新たにアプリケーションクライアントを作成します。作成するアプリケーションクライアントは、クライアントシークレットを使用して認証させるため「従来のウェブアプリケーション」になります。
アプリケーション名には任意の名前で「SecureAppClient」と入力してアプリケーションクライアントを作成します。
作成しましたら、先程と同様で認証方法は、ユーザー名 + パスワードを使用していきますので
「ユーザー名とパスワード (ALLOW_USER_PASSWORD_AUTH) を使用してサインインします」にチェックを入れて保存します。
前回作成したアプリケーションクライアントとは異なり、今回作成したものには「クライアントシークレット」の項目が追加されています。
現状、このアプリケーションクライアントでCurlコマンドを実行すると、
「Client <アプリケーションクライアントのID> is configured with secret but SECRET_HASH was not received」というエラー表示が返ってきます。
つまり、SECRET_HASHがないがためにトークンが取得できないということです。
では、クライアントシークレットをもとに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の値が返ってきます。
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が取得できたことを確認できます。
では、APIへリクエストを送信していきます。
ID トークンを付与し、-H "Authorization: Bearer $TOKEN" をリクエストに追加して送信したところ、{"message": "Hello from Lambda!"} というレスポンスが返されました。
クライアントシークレットありで、Cognitoオーソライザーの設定が正常に動作していることが確認できました。
以上で、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課
川井 康敬