231 lines
7.3 KiB
JavaScript
Executable File
231 lines
7.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Inject authentication cookies/tokens into browser session
|
|
* Usage: node inject-auth.js --url https://example.com --cookies '[{"name":"token","value":"xxx","domain":".example.com"}]'
|
|
* node inject-auth.js --url https://example.com --token "Bearer xxx" [--header Authorization]
|
|
* node inject-auth.js --url https://example.com --local-storage '{"key":"value"}'
|
|
* node inject-auth.js --url https://example.com --session-storage '{"key":"value"}'
|
|
*
|
|
* This script injects authentication data into browser session for testing protected routes.
|
|
* The session persists across script executions until --close true is used.
|
|
*
|
|
* Workflow for testing protected routes:
|
|
* 1. User manually logs into the site in their browser
|
|
* 2. User extracts cookies/tokens from browser DevTools
|
|
* 3. Run this script to inject auth into puppeteer session
|
|
* 4. Run other scripts (screenshot, navigate, etc.) which will use authenticated session
|
|
*
|
|
* Session behavior:
|
|
* --close false : Keep browser running (default for chaining)
|
|
* --close true : Close browser completely and clear session
|
|
*/
|
|
import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError, saveAuthSession, clearAuthSession } from './lib/browser.js';
|
|
|
|
/**
|
|
* Parse cookies from JSON string or file
|
|
* @param {string} cookiesInput - JSON string or file path
|
|
* @returns {Array} - Array of cookie objects
|
|
*/
|
|
function parseCookies(cookiesInput) {
|
|
try {
|
|
// Try parsing as JSON string
|
|
return JSON.parse(cookiesInput);
|
|
} catch {
|
|
throw new Error(`Invalid cookies format. Expected JSON array: [{"name":"cookie_name","value":"cookie_value","domain":".example.com"}]`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse storage data from JSON string
|
|
* @param {string} storageInput - JSON string
|
|
* @returns {Object} - Storage key-value pairs
|
|
*/
|
|
function parseStorage(storageInput) {
|
|
try {
|
|
return JSON.parse(storageInput);
|
|
} catch {
|
|
throw new Error(`Invalid storage format. Expected JSON object: {"key":"value"}`);
|
|
}
|
|
}
|
|
|
|
async function injectAuth() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
if (!args.url) {
|
|
outputError(new Error('--url is required (base URL for the protected site)'));
|
|
return;
|
|
}
|
|
|
|
// Validate at least one auth method provided
|
|
if (!args.cookies && !args.token && !args['local-storage'] && !args['session-storage']) {
|
|
outputError(new Error('At least one auth method required: --cookies, --token, --local-storage, or --session-storage'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const browser = await getBrowser({
|
|
headless: args.headless
|
|
});
|
|
|
|
const page = await getPage(browser);
|
|
|
|
// Navigate to the URL first to set the domain context
|
|
await page.goto(args.url, {
|
|
waitUntil: args['wait-until'] || 'networkidle2',
|
|
timeout: parseInt(args.timeout || '30000')
|
|
});
|
|
|
|
const result = {
|
|
success: true,
|
|
url: args.url,
|
|
injected: []
|
|
};
|
|
|
|
// Inject cookies
|
|
if (args.cookies) {
|
|
const cookies = parseCookies(args.cookies);
|
|
|
|
// Validate and normalize cookies
|
|
const normalizedCookies = cookies.map(cookie => {
|
|
if (!cookie.name || !cookie.value) {
|
|
throw new Error(`Cookie must have 'name' and 'value' properties`);
|
|
}
|
|
|
|
// Extract domain from URL if not provided
|
|
if (!cookie.domain) {
|
|
const urlObj = new URL(args.url);
|
|
cookie.domain = urlObj.hostname;
|
|
}
|
|
|
|
return {
|
|
name: cookie.name,
|
|
value: cookie.value,
|
|
domain: cookie.domain,
|
|
path: cookie.path || '/',
|
|
httpOnly: cookie.httpOnly !== undefined ? cookie.httpOnly : false,
|
|
secure: cookie.secure !== undefined ? cookie.secure : args.url.startsWith('https'),
|
|
sameSite: cookie.sameSite || 'Lax',
|
|
...(cookie.expires && { expires: cookie.expires })
|
|
};
|
|
});
|
|
|
|
await page.setCookie(...normalizedCookies);
|
|
result.injected.push({
|
|
type: 'cookies',
|
|
count: normalizedCookies.length,
|
|
names: normalizedCookies.map(c => c.name)
|
|
});
|
|
}
|
|
|
|
// Inject Bearer token via localStorage (common pattern)
|
|
if (args.token) {
|
|
const tokenKey = args['token-key'] || 'access_token';
|
|
const token = args.token.startsWith('Bearer ') ? args.token.slice(7) : args.token;
|
|
|
|
await page.evaluate((key, value) => {
|
|
localStorage.setItem(key, value);
|
|
}, tokenKey, token);
|
|
|
|
result.injected.push({
|
|
type: 'token',
|
|
key: tokenKey,
|
|
storage: 'localStorage'
|
|
});
|
|
|
|
// Also set Authorization header for future requests if header option provided
|
|
if (args.header) {
|
|
await page.setExtraHTTPHeaders({
|
|
[args.header]: args.token.startsWith('Bearer ') ? args.token : `Bearer ${args.token}`
|
|
});
|
|
result.injected.push({
|
|
type: 'header',
|
|
name: args.header
|
|
});
|
|
}
|
|
}
|
|
|
|
// Inject localStorage items
|
|
if (args['local-storage']) {
|
|
const storageData = parseStorage(args['local-storage']);
|
|
|
|
await page.evaluate((data) => {
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
|
|
});
|
|
}, storageData);
|
|
|
|
result.injected.push({
|
|
type: 'localStorage',
|
|
keys: Object.keys(storageData)
|
|
});
|
|
}
|
|
|
|
// Inject sessionStorage items
|
|
if (args['session-storage']) {
|
|
const storageData = parseStorage(args['session-storage']);
|
|
|
|
await page.evaluate((data) => {
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
sessionStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
|
|
});
|
|
}, storageData);
|
|
|
|
result.injected.push({
|
|
type: 'sessionStorage',
|
|
keys: Object.keys(storageData)
|
|
});
|
|
}
|
|
|
|
// Reload page to apply auth (optional, use --reload true)
|
|
if (args.reload === 'true') {
|
|
await page.reload({ waitUntil: 'networkidle2' });
|
|
result.reloaded = true;
|
|
}
|
|
|
|
// Save auth session to file for persistence across script executions
|
|
const authSessionData = {};
|
|
|
|
if (args.cookies) {
|
|
authSessionData.cookies = parseCookies(args.cookies);
|
|
}
|
|
if (args['local-storage']) {
|
|
authSessionData.localStorage = parseStorage(args['local-storage']);
|
|
}
|
|
if (args['session-storage']) {
|
|
authSessionData.sessionStorage = parseStorage(args['session-storage']);
|
|
}
|
|
if (args.token && args.header) {
|
|
authSessionData.headers = {
|
|
[args.header]: args.token.startsWith('Bearer ') ? args.token : `Bearer ${args.token}`
|
|
};
|
|
}
|
|
|
|
// Clear existing auth if --clear flag used
|
|
if (args.clear === 'true') {
|
|
clearAuthSession();
|
|
result.cleared = true;
|
|
} else if (Object.keys(authSessionData).length > 0) {
|
|
saveAuthSession(authSessionData);
|
|
result.persisted = true;
|
|
}
|
|
|
|
// Verify auth by checking page title and URL after injection
|
|
result.finalUrl = page.url();
|
|
result.title = await page.title();
|
|
|
|
outputJSON(result);
|
|
|
|
// Default: disconnect to keep browser running for session persistence
|
|
if (args.close === 'true') {
|
|
await closeBrowser();
|
|
} else {
|
|
await disconnectBrowser();
|
|
}
|
|
process.exit(0);
|
|
} catch (error) {
|
|
outputError(error);
|
|
}
|
|
}
|
|
|
|
injectAuth();
|