132 lines
5.5 KiB
JavaScript
132 lines
5.5 KiB
JavaScript
|
|
/**
|
||
|
|
* 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();
|