Custom Webhook で Auth0 ログを BigQuery に保存する
はじめに
こんにちは、クラウドエースの第三開発部に所属している康と申します。
今回は、Auth0 の Custom Webhook(Log Streaming)機能を使って、ログを Cloud Run Functions(旧称 GCF)経由で BigQuery(BQ)に保存する方法をご紹介します。
対象読者
- 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
- Auth0 には複数の Log Streams オプションがありますが、できるだけログを漏れなく取得したいと考えていました。
- まず Splunk のトライアル版を試してみたものの、ログ内の description フィールドが取得できず、導入は見送りました。
- Datadog ではログ取得自体には問題がなかったのですが、保存目的だけで利用するにはコスト面の負担が大きく、こちらも採用を見送りました。
- 最終的に、Custom Webhook を使って GCF 経由で BigQuery に保存する構成が最もシンプルかつ柔軟であると判断し、これを採用することにしました。
説明すること/説明しないこと
-
説明すること
- GCF(
package.json
、index.js
)の実装コード - ログに description フィールドが含まれていない場合に備えた補完処理の実装
- Auth0 コンソールでの Webhook(Streams)の設定手順
- GCF(
-
説明しないこと
- GCF のデプロイ手順
- BigQuery 側のテーブル作成やスキーマ設計
- Webhook に対する署名検証や認可などのセキュリティ対策
設定する
package.json
、index.js
)の実装コード
GCF(GCF をデプロイする際には、以下の 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 テーブルに保存します。
// Auth0 のログタイプごとに説明文を定義
const typeToDescription = {
// Login - Failure
f: "Failed login",
fc: "Failed connector login",
// ... (その他のログタイプと説明文)
};
typeToDescription は Auth0 の公式ログタイプ一覧をもとに定義されており、該当する説明がない場合は "Unknown" が入ります。
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
の全文になります。
// 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)の設定手順
Auth0 のコンソールで Monitoring > Streams > Create Log Stream > Custom Webhook をクリックします。
Settings ページから Content Format の JSON Object 形式を選択します。
- Payload URL:(GCF で実装してデプロイした URL)
- Content Format:JSON Object
動作テスト
Auth0 コンソールで Monitoring > Streams > Create Log Stream > Logs を開き、任意のログを1つ選択すると、その詳細ページで表示されます。
- "type": "seacft"
- "description": "Successful exchange of Authorization Code for Access Token"
設定した BigQuery テーブルのプレビューで表示されます。
description カラムに「Successful exchange of Authorization Code for Access Token」という、ログタイプ(data カラム内の "type": "seacft")に対応した適切な説明文が記録されました。
まとめ
Auth0 のログ保持期間には制限がありますが、Log Streaming 機能と Google Cloud を組み合わせることで、より長期的かつ柔軟なログ保存が可能になります。
本記事が同様の課題をお持ちの方の参考になれば幸いです。
Discussion