EventBridgeでAuth0のuser_metadata更新を検知してDynamoDBに同期してみた
リテールアプリ共創部@大阪の岩田です。
Auth0のuser_metadataはとても便利な機能でアプリケーションの要件に合わせて任意の属性を定義できます。このメタデータですが、色々な属性をアプリケーションから利用する場面を考えるとAuth0上だけでメタデータを一元管理するのではなくRDB等のデータベースにメタデータを同期して利用するようなユースケースも多いと思います。データを同期する際は、Auth0上で発生したメタデータの更新を関連システムにどうやって連携するかがポイントになります。このブログではデータ同期の選択肢としてAuth0のLogsをEventBridge経由でLambdaに連携してDynamoDBを更新する構成について紹介します。
概要
こんな構成を作ります。
Auth0のAmazon EventBridge Integrationを利用してAuth0のAPI呼び出しイベントをEventBridgeに送信します。送信されたイベントはEventBridgeルールでフィルタし、ユーザー情報の更新イベントをSQSに送信します。SQSに送信されたメッセージはLambdaからPoll & Invokeし、ペイロード内に含まれるAuth0のユーザー情報をDynamoDBに書き込みます。
やってみる
Log Stream作成
それでは早速やっていきましょう。まずAuth0の管理画面からLog Streamを作成します。詳細は以下のブログを参照してください。注意点としてフィルターにはManagement API - Success
のイベントを含めるようにして下さい。
作成できたらイベントソース名を控えておきます。
SAMテンプレートを作成
続いて関連するAWSリソースのデプロイです。今回はSAMを使ってデプロイすることにしました。テンプレートは以下の通りです。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
Dlq:
Type: AWS::SQS::Queue
Queue:
Type: AWS::SQS::Queue
Properties:
RedrivePolicy:
deadLetterTargetArn: !GetAtt Dlq.Arn
maxReceiveCount: 5
Auth0UserTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: user_id
AttributeType: S
KeySchema:
- AttributeName: user_id
KeyType: HASH
Auth0SyncFunction:
Type: AWS::Serverless::Function
Properties:
Timeout: 30
Handler: app.handler
Runtime: nodejs22.x
CodeUri: ./auth0-sync/
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt Queue.Arn
BatchSize: 10
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref Auth0UserTable
Environment:
Variables:
USER_TABLE: !Ref Auth0UserTable
Auth0EventStreamEventRule:
Type: AWS::Events::Rule
Properties:
# TODO Parametersに外出しする
EventBusName: aws.partner/auth0.com/...略/auth0.logs
EventPattern:
source:
- aws.partner/auth0.com/...略/auth0.logs
detail-type:
- Auth0 log
detail:
data:
type:
- sapi
description:
- Update a User
Targets:
- Id: SQS
Arn: !GetAtt Queue.Arn
EvSendMessageToQueuePolicy:
Type: AWS::SQS::QueuePolicy
Properties:
Queues:
- !Ref Queue
PolicyDocument:
Statement:
- Action:
- sqs:SendMessage
Effect: Allow
Resource: !GetAtt Queue.Arn
Principal:
Service: events.amazonaws.com
EventBusName
とEventPattern
は手抜きしてパラメータ化していないので、先程控えたイベントソース名をオンコードで記述します。ポイントはEventBridgeのルールです。デプロイすると最終的に以下のようなイベントパターンがデプロイされます。
{
"detail-type": ["Auth0 log"],
"source": ["aws.partner/auth0.com/...略/auth0.logs"],
"detail": {
"data": {
"description": ["Update a User"],
"type": ["sapi"]
}
}
}
type
で指定しているsapi
はSuccess API Operationの略で、Auth0のマネジメントAPIによる書き込み操作の成功を意味しています。Log Streamに流れてくるイベント種別については以下のドキュメントに記載されているので、詳細についてはこちらを参照して下さい。
上記type
の指定に加えてdescription
のUpdate a User
のフィルタによってAuth0上でユーザープロファイルが更新されたイベントのみ後続処理に流れるように制御しています。今回はここまでの条件しか指定していませんが同期対象がuser_metadataだけでよければdata.details.request
にuser_metadata
が存在するか?という条件を追加で指定しても良いでしょう。
Log Streamに流れるイベントデータは以下のような構造になっています。これを上記のEventBridgeルールでフィルタする形になります。
{
"log_id": "90020241225053426033422000000000000001223372063659513823",
"data": {
"date": "2024-12-25T05:34:25.916Z",
"type": "sapi",
"description": "Update a User",
"client_id": "SDQbKjcIB8KG8yC0mcO9SSRYYx7pTRdH",
"client_name": "",
"ip": "<ユーザー更新操作のアクセス元IPアドレス>",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"details": {
"request": {
"method": "patch",
"path": "/api/v2/users/auth0%7C676b8ff5a2700970f683c619",
"query": {
},
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"body": {
"user_metadata": {
"key1": "val1",
"key2": "val23"
},
"app_metadata": {
}
},
"channel": "https://manage.auth0.com/",
"ip": "<ユーザー更新操作のアクセス元IPアドレス>",
"auth": {
"user": {
"user_id": "<ユーザーを更新したAuth0テナント管理者のユーザーID>",
"name": "<ユーザーを更新したAuth0テナント管理者のユーザー名>",
"email": "<ユーザーを更新したAuth0テナント管理者のメールアドレス>"
},
"strategy": "jwt",
"credentials": {
"jti": "46ec55157b9a850f1f5fa28da1403ce4",
"scopes": [
"create:actions",
"create:authentication_methods",
"create:client_credentials",
"create:client_grants",
"create:clients",
"create:connections",
"create:custom_domains",
"create:email_provider",
"create:email_templates",
"create:guardian_enrollment_tickets",
"create:integrations",
"create:log_streams",
"create:organization_client_grants",
"create:organization_connections",
"create:organization_invitations",
"create:organization_member_roles",
"create:organization_members",
"create:organizations",
"create:phone_providers",
"create:phone_templates",
"create:requested_scopes",
"create:resource_servers",
"create:roles",
"create:rules",
"create:scim_config",
"create:scim_token",
"create:self_service_profiles",
"create:shields",
"create:signing_keys",
"create:tenant_invitations",
"create:test_email_dispatch",
"create:users",
"delete:actions",
"delete:anomaly_blocks",
"delete:authentication_methods",
"delete:branding",
"delete:client_credentials",
"delete:client_grants",
"delete:clients",
"delete:connections",
"delete:custom_domains",
"delete:device_credentials",
"delete:email_provider",
"delete:email_templates",
"delete:grants",
"delete:guardian_enrollments",
"delete:integrations",
"delete:log_streams",
"delete:organization_client_grants",
"delete:organization_connections",
"delete:organization_invitations",
"delete:organization_member_roles",
"delete:organization_members",
"delete:organizations",
"delete:owners",
"delete:phone_providers",
"delete:phone_templates",
"delete:requested_scopes",
"delete:resource_servers",
"delete:roles",
"delete:rules",
"delete:rules_configs",
"delete:scim_config",
"delete:scim_token",
"delete:self_service_profiles",
"delete:shields",
"delete:tenant_invitations",
"delete:tenant_members",
"delete:tenants",
"delete:users",
"read:actions",
"read:anomaly_blocks",
"read:attack_protection",
"read:authentication_methods",
"read:branding",
"read:checks",
"read:client_credentials",
"read:client_grants",
"read:client_keys",
"read:clients",
"read:connections",
"read:connections_options",
"read:custom_domains",
"read:device_credentials",
"read:email_provider",
"read:email_templates",
"read:email_triggers",
"read:entity_counts",
"read:grants",
"read:guardian_factors",
"read:insights",
"read:integrations",
"read:log_streams",
"read:logs",
"read:mfa_policies",
"read:organization_client_grants",
"read:organization_connections",
"read:organization_invitations",
"read:organization_member_roles",
"read:organization_members",
"read:organizations",
"read:phone_providers",
"read:phone_templates",
"read:prompts",
"read:requested_scopes",
"read:resource_servers",
"read:roles",
"read:rules",
"read:rules_configs",
"read:scim_config",
"read:scim_token",
"read:self_service_profile_custom_texts",
"read:self_service_profiles",
"read:shields",
"read:signing_keys",
"read:stats",
"read:tenant_invitations",
"read:tenant_members",
"read:tenant_settings",
"read:triggers",
"read:users",
"run:checks",
"update:actions",
"update:attack_protection",
"update:authentication_methods",
"update:branding",
"update:client_credentials",
"update:client_grants",
"update:client_keys",
"update:clients",
"update:connections",
"update:connections_options",
"update:custom_domains",
"update:email_provider",
"update:email_templates",
"update:email_triggers",
"update:guardian_factors",
"update:integrations",
"update:log_streams",
"update:mfa_policies",
"update:organization_connections",
"update:organizations",
"update:phone_providers",
"update:phone_templates",
"update:prompts",
"update:requested_scopes",
"update:resource_servers",
"update:roles",
"update:rules",
"update:rules_configs",
"update:scim_config",
"update:self_service_profile_custom_texts",
"update:self_service_profiles",
"update:shields",
"update:signing_keys",
"update:tenant_members",
"update:tenant_settings",
"update:triggers",
"update:users"
]
}
}
},
"response": {
"statusCode": 200,
"body": {
"created_at": "2024-12-25T04:54:13.371Z",
"email": "<メールアドレス>",
"email_verified": true,
"identities": [
{
"connection": "Username-Password-Authentication",
"user_id": "<更新対象のユーザーID ※'auth0|'を含まない>",
"provider": "auth0",
"isSocial": false
}
],
"name": "<ユーザー名>",
"nickname": "iwata.tomoya",
"picture": "https://s.gravatar.com/avatar/119659c28d16f22d01eb48a6f3ee1391?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fiw.png",
"updated_at": "2024-12-25T05:34:25.909Z",
"user_id": "<更新対象のユーザーID>",
"user_metadata": {
"key1": "val1",
"key2": "val23"
}
}
}
},
"user_id": "<ユーザーを更新したAuth0テナント管理者のユーザーID>",
"$event_schema": {
"version": "1.0.0"
},
"log_id": "90020241225053426033422000000000000001223372063659513823",
"tenant_name": "<Auth0のテナント名>"
}
}
色々な項目が含まれていますが、details.response.bodyにユーザープロファイル更新時のレスポンスボディが含まれているので、この項目を参照すれば更新後の最新ユーザー情報が取得できます。
Lambdaの実装
続いてLambdaの実装です。エラーハンドリングなど諸々省略した手抜き実装ですが、雰囲気はつかめると思います。
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
const dynamodbClient = new DynamoDBClient();
const docClient = DynamoDBDocumentClient.from(dynamodbClient);
export const handler = async (event, context) => {
for (const record of event.Records) {
const detail = JSON.parse(record.body).detail;
console.log(detail);
const user = detail.data.details.response.body;
const command = new PutCommand({
TableName: process.env.USER_TABLE,
Item: user
});
await docClient.send(command);
}
};
SQSから取得したイベントデータを1件ずつパースした後 const user = detail.data.details.response.body;
の部分でAuth0のAPI レスポンスからユーザープロファイルを取得し、DynamoDBにPutItemする実装です。
これで一通り準備ができたのでsam build && sam deploy
でデプロイしておきます。
Auth0の管理画面からuser_metadataを更新してみる
ここからは動作確認です。Auth0の管理画面からuser_metadataを更新してみましょう。今回はkey1~key4というメタデータを付与してみました。
少し時間をおいてからDynamoDBのテーブルを確認すると...無事にAuth0のユーザープロファイルが登録されていました!!同期成功です。
実際に案件に導入する際の注意事項
以上。簡単にではありますが、Auth0のuser_metadata変更を検知してDynamoDBに同期してみました。今回は検証目的なのであまり作り込んでいませんが、実際に案件で利用する際は以下のような点にも注意して下さい。
- DLQは適宜設定してださい。
- SQS→Lambdaの部分は部分応答を有効化しておき、処理途中に異常終了した場合は途中からリトライできるようにしておくと良いでしょう。
- SQS→Lambdaが重複起動することも考慮してLambdaの実装は冪等になるよう注意してください。
- レアケースかと思いますが、同一のユーザープロファイルを連続して更新した場合、Lambdaに到達するイベントデータの順序が逆転している可能性があります。DBで最終更新日を管理して適宜更新処理をスキップできるように考慮しましょう。
- ちゃんと動作確認していないのですが、Auth0のLog Streamで指定可能なイベントには
sce(Success Change Email)
などもあります。Management API以外からメールアドレス等の情報が更新されることも考えられるので、必要に応じてsapi
以外のイベントも処理してください。 - DynamoDBに同期する際は雑にPutItemしていますが、ここも要件に応じてUpdateItemに変更するなど調整して下さい。
まとめ
EventBridgeのパートナーイベントソースでAuth0が利用できるのはとても便利ですね!例えばですが、ユーザーのメールアドレス変更を検知してXXしたいとか、Auth0上の全イベントのログをS3に保存したいとか、色々と追加の要件が発生した場合でもEventBridgeルールを追加することで柔軟に対応できそうです。うまく活用していきたいです。