wulf-pulse/scripts/test-duo.mjs

132 lines
5.5 KiB
JavaScript
Raw Permalink Normal View History

/**
* Quick connectivity test for both Duo API integrations.
* Calls a lightweight read endpoint on each:
* Accounts API GET /admin/v1/accounts (lists child accounts)
* Admin API GET /admin/v1/info/summary (tenant summary stats)
*
* Duo auth: HMAC-SHA1 signed "Authorization: Basic <b64(ikey:sig)>" header
* https://duo.com/docs/adminapi#authentication
*/
import crypto from 'crypto';
import https from 'https';
// ── credentials from .env.local ──────────────────────────────────────────────
const ACCOUNTS = {
ikey: process.env.DUOACCOUNTS_INTEGRATION_KEY,
skey: process.env.DUOACCOUNTS_SECRET_KEY,
host: process.env.DUOACCOUNTS_API_HOSTNAME,
};
const ADMIN = {
ikey: process.env.DUOADMIN_INTEGRATION_KEY,
skey: process.env.DUOADMIN_SECRET_KEY,
host: process.env.DUOADMIN_API_HOSTNAME,
};
// ── Duo HMAC signer ───────────────────────────────────────────────────────────
function duoSign(ikey, skey, host, method, path, params = {}) {
const date = new Date().toUTCString();
const sortedParams = Object.keys(params)
.sort()
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join('&');
const canon = [date, method.toUpperCase(), host.toLowerCase(), path, sortedParams].join('\n');
const sig = crypto.createHmac('sha1', skey).update(canon).digest('hex');
const auth = Buffer.from(`${ikey}:${sig}`).toString('base64');
return { date, auth: `Basic ${auth}`, query: sortedParams };
}
// ── generic HTTPS GET ─────────────────────────────────────────────────────────
function duoGet(creds, path, params = {}) {
return new Promise((resolve, reject) => {
const { date, auth, query } = duoSign(creds.ikey, creds.skey, creds.host, 'GET', path, params);
const url = `https://${creds.host}${path}${query ? '?' + query : ''}`;
const req = https.get(url, {
headers: { Authorization: auth, Date: date, 'Content-Type': 'application/json' },
}, res => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
catch { resolve({ status: res.statusCode, data: body }); }
});
});
req.on('error', reject);
req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
});
}
// ── run tests ────────────────────────────────────────────────────────────────
async function main() {
console.log('=== Duo API Connectivity Test ===\n');
// Validate env
for (const [name, val] of Object.entries({ ...ACCOUNTS, ...ADMIN })) {
if (!val) { console.error(`Missing env var for key: ${name}`); process.exit(1); }
}
// 1. Accounts API — POST /accounts/v1/account/list (all Accounts API endpoints use POST)
console.log('── Accounts API ────────────────────────────────');
console.log(`Host : ${ACCOUNTS.host}`);
console.log(`IKey : ${ACCOUNTS.ikey}`);
try {
const r = await duoPost(ACCOUNTS, '/accounts/v1/account/list');
console.log(`HTTP : ${r.status}`);
if (r.status === 200) {
const accounts = r.data?.response ?? [];
console.log(`✓ OK — ${accounts.length} child account(s) returned`);
accounts.slice(0, 5).forEach(a => console.log(` · ${a.name} (${a.account_id}) — ${a.api_hostname}`));
} else {
console.log(`✗ Response:`, JSON.stringify(r.data, null, 2));
}
} catch (e) {
console.log(`✗ Error: ${e.message}`);
}
console.log();
// 2. Admin API — GET /admin/v1/info/summary
console.log('── Admin API ───────────────────────────────────');
console.log(`Host : ${ADMIN.host}`);
console.log(`IKey : ${ADMIN.ikey}`);
try {
const r = await duoGet(ADMIN, '/admin/v1/info/summary');
console.log(`HTTP : ${r.status}`);
if (r.status === 200) {
const s = r.data?.response ?? {};
console.log('✓ OK — Summary:');
console.log(` Users : ${s.user_count ?? 'n/a'}`);
console.log(` Integrations : ${s.integration_count ?? 'n/a'}`);
console.log(` Phones : ${s.telephony_credits_remaining ?? 'n/a'} credits remaining`);
} else {
console.log(`✗ Response:`, JSON.stringify(r.data, null, 2));
}
} catch (e) {
console.log(`✗ Error: ${e.message}`);
}
// 3. Admin API — GET /admin/v1/users (first page, limit 5)
console.log();
console.log('── Admin API — Users (first 5) ─────────────────');
try {
const r = await duoGet(ADMIN, '/admin/v1/users', { limit: '5', offset: '0' });
console.log(`HTTP : ${r.status}`);
if (r.status === 200) {
const users = r.data?.response ?? [];
console.log(`✓ OK — ${users.length} user(s) in page`);
users.forEach(u => console.log(` · ${u.username}${u.status}${u.email ?? '(no email)'}`));
} else {
console.log(`✗ Response:`, JSON.stringify(r.data, null, 2));
}
} catch (e) {
console.log(`✗ Error: ${e.message}`);
}
console.log('\n=== Done ===');
}
main();