Skip to content

Instantly share code, notes, and snippets.

@eirikb
Last active February 15, 2024 14:06
Show Gist options
  • Save eirikb/5393cf74e899c9508f3bdf83bd43a7dd to your computer and use it in GitHub Desktop.
Save eirikb/5393cf74e899c9508f3bdf83bd43a7dd to your computer and use it in GitHub Desktop.

What - Is - This

Say good bye to Postman, Insomnia and even your .http-files. Say hello to programmable API testing.
This is a simple tiny script using a browser to perform token refresh, either up front or headless in background.
You don't need to mess with client ids, secrets, endpoints, OAuth me here and OAuth me there.
Just normal - browser - login.

Usage

npm i puppeteer

import { Q } from "./Q.mjs";

const { fetch, getToken } = Q("./user-data", "https://yourapilogin");

console.log(await fetch("https://yourapi/api/stuff"));
console.log(await getToken());

Initially, Puppeteer's browser window allows for API login. Note, the login endpoint may differ from the API's.
Post-login, token is loaded from cache, or Puppeteer operates headlessly, managing token refreshes until cookies or refresh token expires.

Tricks & Tips

For SSO accounts (e.g., Microsoft, Google, GitHub), reuse the user data directory across APIs to minimize login frequency.

How It Works

fetch from Q enhances the native fetch, invoking getToken upon encountering a 401 (Unauthorized) response:

  1. Call fetch.
  2. If a 401 response occurs, call getToken.
  3. Initiate Puppeteer headlessly, looking for any "Authorization" header.
  4. If no header is found within 2 seconds, switch to a visible Puppeteer window.
  5. Store and return the token once the "Authorization" header is detected.

For subsequent runs, if the authorization token is still valid, the process is streamlined, avoiding unnecessary logins. If the token expires or is revoked, Puppeteer refreshes it using the saved user directory, maintaining seamless access.

import fs from "fs";
import puppeteer from "puppeteer";
export const Q = (userDataDir, tokenHost) => {
const getToken3 = (headless) =>
new Promise(async (resolve, reject) => {
const browser = await puppeteer.launch({
headless,
userDataDir,
});
const page = await browser.newPage();
page.on("request", (request) => {
try {
const headers = request.headers();
if (
headers.authorization &&
headers.authorization.startsWith("Bearer ")
) {
browser.close();
resolve(headers.authorization);
return;
}
page
.cookies()
.then((cookies) => {
for (const cookie of cookies) {
if (cookie.name === "Authorization") {
browser.close();
resolve(`Bearer ${cookie.value}`);
}
}
})
.catch(() => {});
} catch (e) {}
});
await page.goto(tokenHost);
if (headless) {
setTimeout(() => {
browser.close();
reject();
}, 2000);
}
});
const getToken2 = async () => {
try {
return await getToken3(true);
} catch (e) {
return await getToken3(false);
}
};
const getTokenData = () => {
try {
const d = JSON.parse(
fs.readFileSync(`${userDataDir}/token.json`, "utf8")
);
if (typeof d === "object") return d;
} catch (e) {}
return {};
};
const writeTokenData = (tokenData) => {
fs.writeFileSync(
`${userDataDir}/token.json`,
JSON.stringify(tokenData),
"utf8"
);
};
const getToken = async () => {
const tokenData = getTokenData();
if (tokenData[tokenHost]) return tokenData[tokenHost];
const token = await getToken2();
tokenData[tokenHost] = token;
writeTokenData(tokenData);
return token;
};
const _fetch = async (url, init = {}, retry = 1) => {
const token = await getToken();
const res = await fetch(`${url}`, {
...init,
headers: {
...init.headers,
accept: "application/json",
Authorization: token,
},
});
if (res.status === 401 && retry > 0) {
const tokenData = getTokenData();
delete tokenData[tokenHost];
writeTokenData(tokenData);
return _fetch(url, init, retry - 1);
}
if (!res.ok) {
throw `${res.status} ${res.statusText} - ${await res.text()}`;
}
return res;
};
return {
getToken,
fetch: _fetch,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment