-
Notifications
You must be signed in to change notification settings - Fork 23
feat: auth0 audit claude skill #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| .env | ||
| .claudecreds | ||
| .auth0-token-cache | ||
| node_modules/ | ||
| .claude | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Auth0 Security Audit Plugin | ||
|
|
||
| This project is an Auth0 Customer Identity Cloud (CIC) security audit plugin for Claude Code. | ||
| It targets Auth0 CIC only — not Okta Workforce Identity Cloud or Okta Identity Engine. | ||
|
|
||
| **Prerequisites:** | ||
| - Node.js ^20.19.0 || ^22.12.0 || ^24.0.0 | ||
| - `auth0-mcp-server` configured in `.claude/settings.json` (already done — runs via `hooks/mcp-server.js`) | ||
|
|
||
| **Credential handling — IMPORTANT:** | ||
| All Auth0 credential acquisition is handled automatically by `hooks/mcp-server.js`. Do NOT look for tokens in the OS keychain, `~/.config`, or any other system location. Do NOT attempt to run `auth0-mcp-server init` manually. | ||
|
|
||
| The wrapper uses the device authorization flow: | ||
| 1. Checks for an existing token in the OS keychain (stored by a previous `auth0-mcp-server init` run) | ||
| 2. If no token is found, runs `npx @auth0/auth0-mcp-server init` (without options) — this opens the Auth0 device-auth page in the browser and persists the token in the OS keychain | ||
| 3. Spawns `npx @auth0/auth0-mcp-server run` | ||
|
|
||
| The MCP tools (`auth0_list_actions`, `auth0_list_applications`, etc.) are ready to use without any additional auth steps after the first browser-based login. | ||
|
|
||
| **Slash command:** `/auth0-audit` — see `.claude/commands/auth0-audit.md` for full implementation steps. | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| .claude/ | ||
| commands/ | ||
| auth0-audit.md ← slash command definition (skill steps) | ||
| settings.json ← MCP server + hook config | ||
| hooks/ | ||
| inject-auth0-context.js ← SessionStart hook | ||
| mcp-server.js ← MCP server wrapper (token caching) | ||
| tools/ | ||
| render-findings.js ← ActionsWhisperer output renderer | ||
| token-graveyard.js ← TokenGraveyard analysis + scoring | ||
| passkeys-readiness.js ← PasskeysReadiness 4-checkpoint script | ||
| scripts/ | ||
| setup-demo-tenant.js ← Populates dev tenant with synthetic data | ||
| teardown-demo-tenant.js | ||
| demo-cache.json ← Cached API responses for offline demo | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * SessionStart hook — injects Auth0 tenant context into Claude's session. | ||
| * Warns if AUTH0_CLIENT_SECRET is in .env but auth0-mcp-server is not configured. | ||
| * Output is read by Claude Code and prepended to the system context. | ||
| */ | ||
|
|
||
| import { readFileSync, existsSync } from 'fs'; | ||
| import { resolve } from 'path'; | ||
|
|
||
| const cwd = process.cwd(); | ||
|
|
||
| function loadEnvFile(path) { | ||
| if (!existsSync(path)) return {}; | ||
| const vars = {}; | ||
| for (const line of readFileSync(path, 'utf8').split('\n')) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || trimmed.startsWith('#')) continue; | ||
| const eq = trimmed.indexOf('='); | ||
| if (eq === -1) continue; | ||
| vars[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, ''); | ||
| } | ||
| return vars; | ||
| } | ||
|
|
||
| function loadClaudeCreds(path) { | ||
| if (!existsSync(path)) return {}; | ||
| try { | ||
| return JSON.parse(readFileSync(path, 'utf8')); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
|
|
||
| const envVars = loadEnvFile(resolve(cwd, '.env')); | ||
| const claudeCreds = loadClaudeCreds(resolve(cwd, '.claudecreds')); | ||
|
|
||
| const domain = | ||
| process.env.AUTH0_DOMAIN || | ||
| claudeCreds.AUTH0_DOMAIN || | ||
| envVars.AUTH0_DOMAIN || | ||
| null; | ||
|
|
||
| const settingsPath = resolve(cwd, '.claude/settings.json'); | ||
| let mcpConfigured = false; | ||
| if (existsSync(settingsPath)) { | ||
| try { | ||
| const settings = JSON.parse(readFileSync(settingsPath, 'utf8')); | ||
| mcpConfigured = !!(settings.mcpServers && settings.mcpServers.auth0); | ||
| } catch { /* ignore */ } | ||
| } | ||
|
|
||
| const hasStaticSecret = | ||
| !!(envVars.AUTH0_CLIENT_SECRET || process.env.AUTH0_CLIENT_SECRET); | ||
|
|
||
| const warnings = []; | ||
| if (hasStaticSecret && !mcpConfigured) { | ||
| warnings.push( | ||
| 'WARNING: Static Management API credential detected in .env (AUTH0_CLIENT_SECRET). ' + | ||
| 'Consider using auth0-mcp-server for credential-safe access — it stores credentials ' + | ||
| 'in the OS keychain and avoids committing secrets to source control.' | ||
| ); | ||
| } | ||
|
|
||
| const today = new Date().toISOString().split('T')[0]; | ||
|
|
||
| const lines = [ | ||
| '=== Auth0 Security Audit Plugin — Session Context ===', | ||
| '', | ||
| `Date: ${today}`, | ||
| `Tenant: ${domain || '(not configured — set AUTH0_DOMAIN in .env or .claudecreds)'}`, | ||
| '', | ||
| 'Available audit tools (invoke with /auth0-audit):', | ||
| ' --tool actions → ActionsWhisperer: analyze Action JS code for security risks', | ||
| ' --tool tokens → TokenGraveyard: identify dormant/overprivileged M2M apps', | ||
| ' --tool passkeys → PasskeysReadiness: check 4 prerequisites for enabling Passkeys', | ||
| ' --tool all → Run all three tools in sequence (default)', | ||
| '', | ||
| 'Context for judges: Auth0 Actions are server-side JavaScript functions that execute', | ||
| 'at specific points in the authentication pipeline (e.g., after login, during token', | ||
| 'exchange). They can add custom claims, call external services, and modify user', | ||
| 'metadata — making them a critical surface for security review.', | ||
| '', | ||
| 'Scope: Auth0 Customer Identity Cloud (CIC) only.', | ||
| 'All write operations require explicit human approval before execution.', | ||
| ]; | ||
|
|
||
| if (warnings.length > 0) { | ||
| lines.push(''); | ||
| for (const w of warnings) lines.push(w); | ||
| } | ||
|
|
||
| lines.push(''); | ||
| lines.push('=== End Auth0 Context ==='); | ||
|
|
||
| // Claude Code SessionStart hooks output plain text injected into the system context. | ||
| process.stdout.write(lines.join('\n') + '\n'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * MCP server wrapper — ensures the auth0-mcp-server keychain token is | ||
| * initialised (device authorization flow) before spawning the server. | ||
| * | ||
| * On first run (or when the stored token has expired) this script executes | ||
| * `npx @auth0/auth0-mcp-server init`, which opens the Auth0 device-auth | ||
| * page in the browser and persists the resulting token in the OS keychain. | ||
| * Subsequent runs skip `init` and go straight to `run`. | ||
| */ | ||
|
|
||
| import { execSync, spawn } from 'child_process'; | ||
|
|
||
| function keychainTokenExists() { | ||
| try { | ||
| const token = execSync('security find-generic-password -s auth0-mcp -w 2>/dev/null', { | ||
| stdio: ['pipe', 'pipe', 'ignore'], | ||
| timeout: 5000, | ||
| }).toString().trim(); | ||
| return Boolean(token); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // ── Init (device auth) if no keychain token is present ─────────────────────── | ||
|
|
||
| if (!keychainTokenExists()) { | ||
| process.stderr.write('auth0-mcp: no keychain token found — running init (device auth flow)…\n'); | ||
| try { | ||
| execSync('npx @auth0/auth0-mcp-server init', { stdio: 'inherit' }); | ||
| } catch (err) { | ||
| process.stderr.write(`auth0-mcp: init failed: ${err.message}\n`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| // ── Spawn MCP server ────────────────────────────────────────────────────────── | ||
|
|
||
| const child = spawn('npx', ['@auth0/auth0-mcp-server', 'run'], { | ||
| env: process.env, | ||
| stdio: 'inherit', | ||
| }); | ||
|
|
||
| child.on('error', err => { | ||
| process.stderr.write(`auth0-mcp: failed to start auth0-mcp-server: ${err.message}\n`); | ||
| process.exit(1); | ||
| }); | ||
|
|
||
| child.on('exit', code => process.exit(code ?? 0)); | ||
|
|
||
| for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) { | ||
| process.on(sig, () => child.kill(sig)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "hackapalooza-auth0-audit", | ||
| "version": "1.0.0", | ||
| "description": "Auth0 security audit plugin for Claude Code — ActionsWhisperer, TokenGraveyard, PasskeysReadiness", | ||
| "type": "module", | ||
| "engines": { | ||
| "node": "^20.19.0 || ^22.12.0 || ^24.0.0" | ||
| }, | ||
| "scripts": { | ||
| "passkeys": "node tools/passkeys-readiness.js", | ||
| "token-graveyard": "node tools/token-graveyard.js", | ||
| "render-findings": "node tools/render-findings.js", | ||
| "setup-demo": "node scripts/setup-demo-tenant.js", | ||
| "teardown-demo": "node scripts/teardown-demo-tenant.js" | ||
| }, | ||
| "dependencies": { | ||
| "dotenv": "^16.4.7" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * teardown-demo-tenant.js — Block 5 | ||
| * Removes all synthetic demo data created by setup-demo-tenant.js. | ||
| * Deletes Actions and M2M apps whose names start with 'demo-'. | ||
| * | ||
| * Usage: node scripts/teardown-demo-tenant.js [--dry-run] | ||
| * Env: AUTH0_DOMAIN, AUTH0_TOKEN | ||
| */ | ||
|
|
||
| import { readFileSync, existsSync } from 'fs'; | ||
| import { resolve } from 'path'; | ||
|
|
||
| const args = process.argv.slice(2); | ||
| const DRY_RUN = args.includes('--dry-run'); | ||
|
|
||
| function loadEnv() { | ||
| const path = resolve(process.cwd(), '.env'); | ||
| if (!existsSync(path)) return; | ||
| for (const line of readFileSync(path, 'utf8').split('\n')) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || trimmed.startsWith('#')) continue; | ||
| const eq = trimmed.indexOf('='); | ||
| if (eq === -1) continue; | ||
| const key = trimmed.slice(0, eq).trim(); | ||
| const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, ''); | ||
| if (!process.env[key]) process.env[key] = val; | ||
| } | ||
| } | ||
|
|
||
| loadEnv(); | ||
|
|
||
| const DOMAIN = process.env.AUTH0_DOMAIN; | ||
| const TOKEN = process.env.AUTH0_TOKEN || process.env.AUTH0_MANAGEMENT_TOKEN; | ||
|
|
||
| if (!DOMAIN || !TOKEN) { | ||
| console.error('Error: AUTH0_DOMAIN and AUTH0_TOKEN must be set in environment or .env'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| async function apiFetch(method, path, retries = 3) { | ||
| const url = `https://${DOMAIN}${path}`; | ||
| if (DRY_RUN) { | ||
| console.log(`[DRY RUN] ${method} ${url}`); | ||
| return []; | ||
| } | ||
|
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let Because Minimal fix- if (DRY_RUN) {
+ if (DRY_RUN && method !== 'GET') {
console.log(`[DRY RUN] ${method} ${url}`);
return [];
}Also applies to: 66-68, 84-86 🤖 Prompt for AI Agents |
||
| for (let attempt = 0; attempt <= retries; attempt++) { | ||
| const res = await fetch(url, { | ||
| method, | ||
| headers: { Authorization: `Bearer ${TOKEN}` }, | ||
| }); | ||
| if (res.status === 429) { | ||
| await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 500)); | ||
| continue; | ||
| } | ||
| if (res.status === 404 || res.status === 204) return []; | ||
| if (!res.ok) throw new Error(`${method} ${path} → ${res.status}`); | ||
| const text = await res.text(); | ||
| return text ? JSON.parse(text) : []; | ||
| } | ||
| return []; | ||
| } | ||
|
|
||
| async function deleteActions() { | ||
| console.log('\n→ Deleting demo Actions...'); | ||
| const data = await apiFetch('GET', '/api/v2/actions/actions?per_page=100'); | ||
| const actions = Array.isArray(data) ? data : (data.actions ?? []); | ||
| const demoActions = actions.filter(a => a.name?.startsWith('demo-')); | ||
|
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Paginate the list endpoints before reporting teardown success. Both discovery calls stop at the first 100 records. In a larger tenant, demo Actions or M2M apps beyond page 1 are left behind even though the script prints a clean completion message. Also applies to: 84-86 🤖 Prompt for AI Agents |
||
|
|
||
| for (const action of demoActions) { | ||
| try { | ||
| await apiFetch('DELETE', `/api/v2/actions/actions/${action.id}`); | ||
| console.log(` ✓ Deleted action: ${action.name}`); | ||
| } catch (e) { | ||
| console.warn(` ✗ ${action.name}: ${e.message}`); | ||
| } | ||
| } | ||
|
|
||
| if (demoActions.length === 0) console.log(' (no demo Actions found)'); | ||
| } | ||
|
|
||
| async function deleteM2MApps() { | ||
| console.log('\n→ Deleting demo M2M applications...'); | ||
| const data = await apiFetch('GET', '/api/v2/clients?app_type=non_interactive&per_page=100&fields=client_id,name'); | ||
| const clients = Array.isArray(data) ? data : (data.clients ?? []); | ||
| const demoClients = clients.filter(c => c.name?.startsWith('demo-')); | ||
|
|
||
| for (const client of demoClients) { | ||
| try { | ||
| // Delete client grants first | ||
| const grants = await apiFetch('GET', `/api/v2/client-grants?client_id=${client.client_id}`); | ||
| for (const grant of (Array.isArray(grants) ? grants : [])) { | ||
| await apiFetch('DELETE', `/api/v2/client-grants/${grant.id}`); | ||
| } | ||
| await apiFetch('DELETE', `/api/v2/clients/${client.client_id}`); | ||
| console.log(` ✓ Deleted app: ${client.name}`); | ||
| } catch (e) { | ||
| console.warn(` ✗ ${client.name}: ${e.message}`); | ||
| } | ||
| } | ||
|
|
||
| if (demoClients.length === 0) console.log(' (no demo M2M apps found)'); | ||
| } | ||
|
|
||
| console.log(`Auth0 Demo Tenant Teardown${DRY_RUN ? ' [DRY RUN]' : ''}`); | ||
| console.log(`Tenant: ${DOMAIN}`); | ||
| console.log('NOTE: This does NOT restore tenant settings (Login mode, WebAuthn factors).'); | ||
| console.log(' Restore manually if needed: Auth0 Dashboard → Settings.'); | ||
|
|
||
| await deleteActions(); | ||
| await deleteM2MApps(); | ||
|
|
||
| console.log('\n✅ Teardown complete. Demo data removed.'); | ||
Uh oh!
There was an error while loading. Please reload this page.