Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions plugins/auth0/skills/auth0-audit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.env
.claudecreds
.auth0-token-cache
node_modules/
.claude
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
40 changes: 40 additions & 0 deletions plugins/auth0/skills/auth0-audit/CLAUDE.md
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
```
97 changes: 97 additions & 0 deletions plugins/auth0/skills/auth0-audit/hooks/inject-auth0-context.js
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');
54 changes: 54 additions & 0 deletions plugins/auth0/skills/auth0-audit/hooks/mcp-server.js
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));
}
19 changes: 19 additions & 0 deletions plugins/auth0/skills/auth0-audit/package.json
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"
}
}
113 changes: 113 additions & 0 deletions plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Let --dry-run read state and skip only destructive calls.

Because apiFetch() returns [] for every method, dry-run never discovers any demo resources and always behaves like the tenant is already clean. The preview mode should still execute GETs and no-op only the DELETEs.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js` around
lines 43 - 46, The DRY_RUN branch in apiFetch currently returns an empty array
for every method so preview mode never discovers resources; change it to only
short-circuit destructive calls: inside the apiFetch function (check the
DRY_RUN, method and url variables), if DRY_RUN is true and method is a
destructive verb (at minimum "DELETE", and optionally "POST"/"PUT"/"PATCH" if
appropriate), log `[DRY RUN] ${method} ${url}` and return [] but allow "GET"
(and other non-destructive methods) to proceed to the real fetch so preview can
list resources; update the DRY_RUN checks at the other locations referenced (the
blocks around lines with the same pattern) to follow the same rule.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js` around
lines 66 - 68, The script only fetches the first page (per_page=100) from list
endpoints; update the apiFetch calls that populate actions and M2M apps to
paginate until no more results. For the actions endpoint (the apiFetch('GET',
'/api/v2/actions/actions?per_page=100') call that feeds the actions and
demoActions variables) implement a loop that requests successive pages (e.g.,
page=0,1,2 or page=1,2,3 depending on API), accumulating results into the
actions array and stopping when a page returns an empty array or fewer than
per_page items; do the same for the client/M2M apps endpoint used around lines
84-86 so you collect all clients before filtering demo apps. Only proceed to
deletion and print the completion message after all pages have been fetched and
processed.


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.');
Loading
Loading