EventBridgeでAuth0のuser_metadata更新を検知してDynamoDBに同期してみた

EventBridgeでAuth0のuser_metadata更新を検知してDynamoDBに同期してみた

Clock Icon2024.12.25

リテールアプリ共創部@大阪の岩田です。

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のイベントを含めるようにして下さい。

https://dev.classmethod.jp/articles/auth0-log-streams-to-amazon-eventbridge-with-cloudformation/

作成できたらイベントソース名を控えておきます。

Log Stream

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

EventBusNameEventPatternは手抜きしてパラメータ化していないので、先程控えたイベントソース名をオンコードで記述します。ポイントは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に流れてくるイベント種別については以下のドキュメントに記載されているので、詳細についてはこちらを参照して下さい。

https://auth0.com/docs/deploy-monitor/logs/log-event-type-codes

上記typeの指定に加えてdescriptionUpdate a UserのフィルタによってAuth0上でユーザープロファイルが更新されたイベントのみ後続処理に流れるように制御しています。今回はここまでの条件しか指定していませんが同期対象がuser_metadataだけでよければdata.details.requestuser_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というメタデータを付与してみました。

user_metadataの更新

少し時間をおいてからDynamoDBのテーブルを確認すると...無事にAuth0のユーザープロファイルが登録されていました!!同期成功です。

DynamoDBに登録されたユーザー情報

実際に案件に導入する際の注意事項

以上。簡単にではありますが、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ルールを追加することで柔軟に対応できそうです。うまく活用していきたいです。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.