Amazon Cognitoでログイン履歴を取得・管理する方法を考えてみた
はじめに
Amazon Cognitoを使用していると、ログイン履歴を取得・管理したくなることがあります。たとえば、ユーザID・メールアドレス・ログイン日時、などです。本記事では、Cognitoのログイン履歴を取得・管理するための具体的な方法を紹介します。
ユーザアクティビティログを使用する方法
CognitoのPlusプランに切り替えて脅威保護機能を有効にすると、ユーザアクティビティログをエクスポートできるようになります。これにより、ログイン履歴をリアルタイムで取得することが可能となります。
ただし、出力されるログにはユーザ属性(たとえばemailアドレスなど)が含まれていません。そこで今回は、ログをKinesis Data Firehose経由でLambda関数に渡し、ユーザ属性を取得してからS3に保存する構成を考えました。
以下は、Firehoseから受け取ったレコードにユーザ属性(email)を付与してS3に保存するLambda関数のサンプルコードです(Python 3.13)
import boto3
import os
import base64
import json
from typing import List, Dict, Any
def get_user_attributes_by_sub(
user_pool_id: str, sub: str, region: str = "ap-northeast-1"
) -> List[Dict[str, Any]]:
"""
指定したユーザプールIDとsubからユーザ属性を取得する関数
"""
client = boto3.client("cognito-idp", region_name=region)
try:
response = client.admin_get_user(UserPoolId=user_pool_id, Username=sub)
return response.get("UserAttributes", [])
except Exception as e:
print(f"ユーザ属性の取得中にエラー: {e}")
raise
def lambda_handler(event: dict, context) -> dict:
"""
Firehoseから呼び出されるLambdaのメイン関数
"""
# リージョンを環境変数から取得(なければ東京リージョン)
region = os.environ.get("AWS_REGION")
if region is None:
region = "ap-northeast-1"
# Firehoseから渡されるレコード配列を取得
records = event.get("records", [])
output = []
for record in records:
record_id = record["recordId"]
result = "ProcessingFailed"
new_data = record["data"]
try:
# レコードのdataはBase64エンコードされているのでデコード
encoded_data = record["data"]
decoded_data = base64.b64decode(encoded_data).decode("utf-8")
data_json = json.loads(decoded_data)
# messageフィールドを取得
message = data_json.get("message", {})
# userSubを取得
user_sub = message.get("userSub")
# userPoolIdを取得(messageまたはlogSourceIdから)
user_pool_id = message.get("userPoolId")
if user_pool_id is None:
log_source = data_json.get("logSourceId", {})
user_pool_id = log_source.get("userPoolId")
# 必須情報がなければ次のレコードへ
if user_pool_id is None or user_sub is None:
output.append({
"recordId": record_id,
"result": result,
"data": new_data
})
continue
# Cognitoからemail属性を取得
attributes = get_user_attributes_by_sub(user_pool_id, user_sub, region)
email = None
for attr in attributes:
if attr.get("Name") == "email":
email = attr.get("Value")
break
# emailをmessageに追加
message["email"] = email
data_json["message"] = message
# 変換後のデータをエンコード
new_payload = json.dumps(data_json) + "\n"
new_data = base64.b64encode(new_payload.encode("utf-8")).decode("utf-8")
result = "Ok"
except Exception as e:
# 例外発生時は失敗扱い
print(f"レコード処理中にエラー: {e}")
# Firehose仕様の返却形式で出力
output.append({
"recordId": record_id,
"result": result,
"data": new_data
})
return {"records": output}
一見すると良い感じに見えますが、この実装には注意点があります。
- 各レコードごとにCognitoのAPI(admin_get_user)を呼び出すため、ユーザ数やログイン回数が多い場合、APIの制限(スロットリング)に到達してしまう
- 一時的にキャッシュを使ってAPI呼び出し回数を減らすこともできるが、根本的な解決策にはならない
大規模な運用や高頻度のログインが発生する環境では、別のアプローチを検討する必要があります。
Lambdaトリガーを使用する方法
CognitoのAPIを個別に呼び出さずにログイン履歴を記録したい場合は、Lambdaトリガー(Post Authentication)を活用する方法が有効です。このトリガーは、ユーザが認証に成功し、トークンを受け取る直前にLambda関数を実行します。これにより、ログイン履歴をリアルタイムで記録できます。
ここでは、認証後トリガーとしてLambda関数を設定し、ユーザがログインした際にCloudWatch Logsへログイン情報を出力する例を紹介します(Python 3.13)
import json
import time
def lambda_handler(event, context):
"""
Cognito Post Authenticationトリガー用Lambdaサンプル。
サインイン時にuserSub, email, eventType, timestampをCloudWatch Logsに出力する。
"""
user_sub = event.get("userName")
user_attributes = event.get("request", {}).get("userAttributes", {})
email = user_attributes.get("email")
event_type = "SignIn"
timestamp = str(int(time.time()))
log_data = {
"userSub": user_sub,
"email": email,
"eventType": event_type,
"timestamp": timestamp,
}
print(json.dumps(log_data)) # CloudWatch Logsに出力
return event
CloudWatch Logs以外にもDynamoDBやS3に記録することで、
- 検索性の向上
- 長期保存
- 他システムとの連携
などのメリットが受けられるようになります。用途に応じて保存先を選択してください。
気になる点としては、Lambda関数内で取得している時間が本当にログイン時間なのか、ということです。あくまでもトリガーが実行された時間であり、ユーザが認証を完了した時間ではない、と思う方もいるかもしれません。
事実、Lambda関数の起動や実行にはごくわずかな遅延が発生します。が、通常は数ミリ秒程度です。Lambda関数内でタイムスタンプを記録すれば、それは「認証完了直後~トークン発行直前」の時刻となり、ほぼログイン時間とみなしても良いかと思います。もちろん、要件によっては注意が必要であることには変わりません。
また、この方法であればCognitoのPlusプランに切り替える必要もないので、コスト面でのメリットもあります。
まとめ
ログストリーミングではユーザ属性は取得できませんが、IPアドレスやデバイス情報などのセキュリティ観点で重要な情報は含まれています。一方、ログイン履歴の管理やユーザごとの分析には、Lambdaトリガーを使った方法がシンプルかつ効果的です。どちらの方法も一長一短があるため、目的に応じて使い分けることをおすすめします。