💨

Custom Webhook で Auth0 ログを BigQuery に保存する

に公開

はじめに

こんにちは、クラウドエースの第三開発部に所属している康と申します。
今回は、Auth0 の Custom Webhook(Log Streaming)機能を使って、ログを Cloud Run Functions(旧称 GCF)経由で BigQuery(BQ)に保存する方法をご紹介します。

https://auth0.com/pricing

対象読者

  • Auth0 のログ保存期間に課題を感じている方
  • Auth0 のログ保存のために Splunk や Datadog を検討したが断念した方
  • GCF と BigQuery を活用したログ保存に興味がある方

背景

  • Auth0 の Essentials プランを利用し始めたところ、ログの保存期間がわずか 5 日間であることが分かりました。

  • さらに、Enterprise プランでも最大で30日間しか保持されないため、より長期間、可能であれば無期限でログを保存できる方法を検討することにしました。

    Plan Log Retention
    Starter 1 day
    B2C Essentials 5 days
    B2C Professional 10 days
    B2B Essentials 5 days
    B2B Professional 10 days
    Enterprise 30 days

https://auth0.com/docs/deploy-monitor/logs/log-data-retention

  • Auth0 には複数の Log Streams オプションがありますが、できるだけログを漏れなく取得したいと考えていました。
  • まず Splunk のトライアル版を試してみたものの、ログ内の description フィールドが取得できず、導入は見送りました。
  • Datadog ではログ取得自体には問題がなかったのですが、保存目的だけで利用するにはコスト面の負担が大きく、こちらも採用を見送りました。
  • 最終的に、Custom Webhook を使って GCF 経由で BigQuery に保存する構成が最もシンプルかつ柔軟であると判断し、これを採用することにしました。

https://auth0.com/docs/customize/log-streams

説明すること/説明しないこと

  • 説明すること

    • GCF(package.jsonindex.js)の実装コード
    • ログに description フィールドが含まれていない場合に備えた補完処理の実装
    • Auth0 コンソールでの Webhook(Streams)の設定手順
  • 説明しないこと

    • GCF のデプロイ手順
    • BigQuery 側のテーブル作成やスキーマ設計
    • Webhook に対する署名検証や認可などのセキュリティ対策

設定する

GCF(package.jsonindex.js)の実装コード

GCF をデプロイする際には、以下の package.json ファイルを用意し、必要な依存関係を管理しています。

package.json
{
  "name": "auth0-logs-to-bigquery",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@google-cloud/bigquery": "^7.3.0"
  }
}

上記の package.json ファイルと同じディレクトリで以下のコマンドを実行し、BigQuery クライアントライブラリをインストールしました。

npm install

これにより、GCF の実行環境で @google-cloud/bigquery が利用可能になります。

ログに description フィールドが含まれていない場合に備えた補完処理の実装

ログの description フィールドは Webhook 方式でも取得できませんでした。
そのため、公式ドキュメントを参照し、イベントコード(ログタイプ)に応じた Description(説明文)を GCF のコード内で定義する方式に変更しました。
そして、その Description を BigQuery テーブルに保存します。
https://auth0.com/docs/customize/log-streams/event-filters

index.json の一部
// Auth0 のログタイプごとに説明文を定義
const typeToDescription = {
  // Login - Failure
  f: "Failed login",
  fc: "Failed connector login",
  // ... (その他のログタイプと説明文)
};

typeToDescription は Auth0 の公式ログタイプ一覧をもとに定義されており、該当する説明がない場合は "Unknown" が入ります。

index.json の一部
const rows = logs.map((log) => {
  // description が空なら、type に基づいて自動補完
  // log.data.type は Auth0 のログ構造の中で "type" を示す部分
  const description =
    log.description || typeToDescription[log.data.type] || "Unknown";

  return {
    log_id: log.log_id,
    description, // type から自動補完した説明を保存
    data: JSON.stringify(log), // ログ全体を JSON 文字列で保存
  };
});

以下は、index.js の全文になります。

index.js
// Google Cloud BigQuery クライアントライブラリをインポート
const { BigQuery } = require("@google-cloud/bigquery");

// BigQuery クライアントを初期化(プロジェクトIDを指定)
const bigquery = new BigQuery({ projectId: "CHANGE_ME" });

// 使用するデータセットとテーブルを指定
const datasetId = "CHANGE_ME";
const tableId = "CHANGE_ME";

// Auth0 のログタイプごとに説明文を定義
const typeToDescription = {
  // Login - Failure
  f: "Failed login",
  fc: "Failed connector login",
  fco: "Origin is not in the application's Allowed Origins list",
  fcoa: "Failed cross-origin authentication",
  fens: "Failed native social login",
  fp: "Incorrect password",
  fu: "Invalid email or username",

  // Login - Notification
  w: "Warnings during login",

  // Login - Success
  s: "Successful login",
  scoa: "Successful cross-origin authentication",
  sens: "Successful native social login",

  // Logout - Failure
  flo: "User logout failed",
  oidc_backchannel_logout_failed: "Failed OIDC back-channel logout request",

  // Logout - Success
  slo: "User successfully logged out",
  oidc_backchannel_logout_succeeded:
    "Successful OIDC back-channel logout request",

  // Signup - Failure
  fs: "User signup failed",

  // Signup - Success
  ss: "Successful user signup",

  // Silent Authentication - Failure
  fsa: "Failed silent authentication",

  // Silent Authentication - Success
  ssa: "Successful silent authentication",

  // Token Exchange - Failure
  feacft: "Failed exchange of Authorization Code for Access Token",
  feccft: "Failed exchange of Access Token for a Client Credentials Grant",
  fede: "Failed exchange of Device Code for Access Token",
  feoobft: "Failed exchange of Password and OOB Challenge for Access Token",
  feotpft: "Failed exchange of Password and OTP Challenge for Access Token",
  fepft: "Failed exchange of Password for Access Token",
  fepotpft: "Failed exchange of Passwordless OTP for Access Token",
  fercft: "Failed exchange of Password and MFA Recovery code for Access Token",
  ferrt: "Failed exchange of Rotating Refresh Token",
  fertft: "Failed exchange of Refresh Token for Access Token",

  // Token Exchange - Success
  seacft: "Successful exchange of Authorization Code for Access Token",
  seccft: "Successful exchange of Access Token for a Client Credentials Grant",
  sede: "Successful exchange of Device Code for Access Token",
  seoobft: "Successful exchange of Password and OOB Challenge for Access Token",
  seotpft: "Successful exchange of Password and OTP Challenge for Access Token",
  sepft: "Successful exchange of Password for Access Token",
  sercft:
    "Successful exchange of Password and MFA Recovery code for Access Token",
  sertft: "Successful exchange of Refresh Token for Access Token",

  // Management API - Failure
  fapi: "Failed Management API operation",

  // Management API - Success
  sapi: "Successful Management API operation",
  mgmt_api_read: "API GET operation returning secrets completed successfully",

  // System - Notification
  admin_update_launch: "Auth0 Update Launched",
  api_limit:
    "The maximum number of requests to the Authentication or Management APIs in given time has reached",
  coff: "AD/LDAP Connector is offline",
  con: "AD/LDAP Connector is online and working",
  depnote: "Deprecation Notice",
  fcpro: "Failed to provision a AD/LDAP connector",
  fui: "Failed to import users",
  limit_delegation: "Rate limit exceeded to /delegation endpoint",
  limit_mu:
    "An IP address is blocked with 100 failed login attempts using different usernames, all with incorrect passwords in 24 hours, or 50 sign-up attempts per minute from the same IP address",
  limit_wc:
    "An IP address is blocked with 10 failed login attempts into a single account from the same IP address",
  sys_os_update_start: "Auth0 OS Update Started",
  sys_os_update_end: "Auth0 OS Update Ended",
  sys_update_start: "Auth0 Update Started",
  sys_update_end: "Auth0 Update Ended",

  // User/Behavioral - Failure
  fce: "Failed to change user email",
  fcp: "Failed to change password",
  fcpn: "Failed to change phone number",
  fcpr: "Failed change password request",
  fcu: "Failed to change username",
  fd: "Failed to generate delegation token",
  fdeaz: "Device authorization request failed",
  fdecc: "User did not confirm device",
  fdu: "Failed user deletion",
  fn: "Failed to send email notification",
  fv: "Failed to send verification email",
  fvr: "Failed to process verification email request",

  // User/Behavioral - Notification
  cs: "Passwordless login code has been sent",
  du: "User has been deleted",
  gd_enrollment_complete:
    "A first time MFA user has successfully enrolled using one of the factors",
  gd_start_enroll: "Multi-factor authentication enroll has started",
  gd_unenroll:
    "Device used for second factor authentication has been unenrolled",
  gd_update_device_account:
    "Device used for second factor authentication has been updated",
  ublkdu: "User block setup by anomaly detection has been released",

  // User/Behavioral - Success
  sce: "Successfully changed user email",
  scp: "Successfully changed password",
  scpn: "Successfully changed phone number",
  scpr: "Successful change password request",
  scu: "Successfully changed username",
  sdu: "User successfully deleted",
  srrt: "Successfully revoked a Refresh Token",
  sui: "Successfully imported users",
  sv: "Successfully consumed email verification link",
  svr: "Successfully called verification email endpoint, verification email in queue.",

  // SCIM Events
  sscim: "Successful SCIM operation",
  fscim: "Failed SCIM operation",

  // Other Events
  organization_member_added: "Successfully added member to organization",
  si: "User invitation accepted",
};

// Cloud Function 本体
exports.saveLogsToBigQuery = async (req, res) => {
  try {
    const logs = req.body.logs;
    // logs が配列でない場合はエラー
    if (!Array.isArray(logs)) {
      res.status(400).send('Invalid log format: "logs" must be an array');
      return;
    }

    // BigQuery に挿入するデータを整形
    const rows = logs.map((log) => {
      // description が空なら、type に基づいて自動補完
      // log.data.type は Auth0 のログ構造の中で "type" を示す部分
      const description =
        log.description || typeToDescription[log.data.type] || "Unknown";

      return {
        log_id: log.log_id,
        description, // type から自動補完した説明を保存
        data: JSON.stringify(log), // ログ全体を JSON 文字列で保存
      };
    });

    // BigQuery にデータを挿入
    await bigquery.dataset(datasetId).table(tableId).insert(rows);

    console.log(`Successfully inserted ${rows.length} rows into ${tableId}.`);
    res.status(200).send(`Successfully processed ${rows.length} logs.`);
  } catch (error) {
    // エラーハンドリング
    console.error("Error inserting logs into BigQuery:", error);
    res.status(500).send("Failed to process logs.");
  }
};

Auth0 コンソールでの Webhook(Streams)の設定手順

Alt text
Auth0 のコンソールで Monitoring > Streams > Create Log Stream > Custom Webhook をクリックします。

Alt text
Settings ページから Content Format の JSON Object 形式を選択します。

  • Payload URL:(GCF で実装してデプロイした URL)
  • Content Format:JSON Object

https://cloud.google.com/bigquery/docs/json-data?hl=ja

動作テスト

Alt text
Auth0 コンソールで Monitoring > Streams > Create Log Stream > Logs を開き、任意のログを1つ選択すると、その詳細ページで表示されます。

  • "type": "seacft"
  • "description": "Successful exchange of Authorization Code for Access Token"

Alt text
設定した BigQuery テーブルのプレビューで表示されます。
description カラムに「Successful exchange of Authorization Code for Access Token」という、ログタイプ(data カラム内の "type": "seacft")に対応した適切な説明文が記録されました。

まとめ

Auth0 のログ保持期間には制限がありますが、Log Streaming 機能と Google Cloud を組み合わせることで、より長期的かつ柔軟なログ保存が可能になります。
本記事が同様の課題をお持ちの方の参考になれば幸いです。

Discussion