import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdtemp, mkdir, readFile, stat, writeFile, readdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { request as httpRequest } from 'node:http';
import { createConnection } from 'node:net';
import { PassThrough } from 'node:stream';
import test, { after } from 'node:test';
import { Auth } from '../src/auth.js';
const future = () => new Date(Date.now() + 3600_000).toISOString();
function mockFetch(handler) {
const calls = [];
const fn = async (url, init) => {
const params = Object.fromEntries(new URLSearchParams(init.body));
calls.push({ url, params, init });
const { status = 200, body } = await handler(params);
return { ok: status < 400, status, json: async () => body };
};
fn.calls = calls;
return fn;
}
const ok = () => ({ body: { access_token: 'new-token', expires_in: 3600 } });
const homes = [];
after(async () => {
await Promise.all(homes.map((dir) => rm(dir, { recursive: true, force: true })));
});
async function home() {
const dir = await mkdtemp(join(tmpdir(), 'colab-auth-'));
homes.push(dir);
return { dir, credsPath: join(dir, '.config', 'colab-mcp', 'credentials.json') };
}
async function writeCreds(path, extra = {}) {
await mkdir(join(path, '..'), { recursive: true });
await writeFile(path, JSON.stringify({ type: 'authorized_user', refresh_token: 'r1', ...extra }));
}
test('COLAB_ACCESS_TOKEN overrides everything', async () => {
const fetch = mockFetch(() => assert.fail('no network'));
const auth = new Auth({ env: { COLAB_ACCESS_TOKEN: 'env-tok' }, fetch });
assert.equal(await auth.token(), 'env-tok');
});
test('missing credentials gives an actionable error', async () => {
const { dir } = await home();
const auth = new Auth({ env: { HOME: dir }, fetch: mockFetch(ok) });
await assert.rejects(auth.token(), /no colab credentials.*login/);
});
test('valid cached token is returned without refresh', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath, { token: 'cached', expiry: future() });
const fetch = mockFetch(ok);
assert.equal(await new Auth({ env: { HOME: dir }, fetch }).token(), 'cached');
assert.equal(fetch.calls.length, 0);
});
test('token inside the expiry skew is refreshed and persisted with 0600', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath, {
token: 'old',
expiry: new Date(Date.now() + 10_000).toISOString(),
});
const fetch = mockFetch(ok);
assert.equal(await new Auth({ env: { HOME: dir }, fetch }).token(), 'new-token');
assert.equal(fetch.calls[0].url, 'https://oauth2.googleapis.com/token');
assert.equal(fetch.calls[0].params.grant_type, 'refresh_token');
assert.ok(fetch.calls[0].init.signal);
const saved = JSON.parse(await readFile(credsPath, 'utf8'));
assert.equal(saved.token, 'new-token');
assert.equal(saved.refresh_token, 'r1');
assert.equal((await stat(credsPath)).mode & 0o777, 0o600);
assert.equal((await stat(join(credsPath, '..'))).mode & 0o777, 0o700);
assert.deepEqual(await readdir(join(credsPath, '..')), ['credentials.json']);
});
test('concurrent token() calls share one refresh', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath);
const fetch = mockFetch(async () => {
await new Promise((r) => setTimeout(r, 20));
return ok();
});
const auth = new Auth({ env: { HOME: dir }, fetch });
const tokens = await Promise.all([auth.token(), auth.token(), auth.token()]);
assert.deepEqual(tokens, ['new-token', 'new-token', 'new-token']);
assert.equal(fetch.calls.length, 1);
});
test('external COLAB_CREDENTIALS is never written and token_uri is ignored', async () => {
const { dir } = await home();
const path = join(dir, 'token.json');
await writeCreds(path, {
token_uri: 'https://evil.example/token',
client_id: 'file-id',
client_secret: 'file-secret',
});
const before = await readFile(path, 'utf8');
const fetch = mockFetch(ok);
const auth = new Auth({ env: { COLAB_CREDENTIALS: path, HOME: dir }, fetch });
assert.equal(await auth.token(), 'new-token');
assert.equal(fetch.calls[0].url, 'https://oauth2.googleapis.com/token');
assert.equal(fetch.calls[0].params.client_id, 'file-id');
assert.equal(await readFile(path, 'utf8'), before);
assert.equal(await auth.token(), 'new-token');
assert.equal(fetch.calls.length, 1);
});
test('COLAB_CLIENT_ID/SECRET override the file and default', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath, { client_id: 'file-id' });
const fetch = mockFetch(ok);
const env = { HOME: dir, COLAB_CLIENT_ID: 'env-id', COLAB_CLIENT_SECRET: 'env-secret' };
await new Auth({ env, fetch }).token();
assert.equal(fetch.calls[0].params.client_id, 'env-id');
assert.equal(fetch.calls[0].params.client_secret, 'env-secret');
});
test('refresh failure points at login and leaks no secrets', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath, { refresh_token: 'super-secret-refresh' });
const fetch = mockFetch(() => ({
status: 400,
body: { error: 'invalid_grant', refresh_token: 'super-secret-refresh' },
}));
await assert.rejects(new Auth({ env: { HOME: dir }, fetch }).token(), (err) => {
assert.match(err.message, /invalid_grant.*login/);
assert.ok(!err.message.includes('super-secret'));
return true;
});
});
test('network failure is reported without the cause', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath);
const fetch = async () => {
throw new Error('connect failed token=abc');
};
await assert.rejects(new Auth({ env: { HOME: dir }, fetch }).token(), (err) => {
assert.match(err.message, /network error/);
assert.ok(!err.message.includes('abc'));
return true;
});
});
test('manual login keeps the sdk paste flow and stores credentials', async () => {
const { dir, credsPath } = await home();
const fetch = mockFetch((p) => {
assert.equal(p.grant_type, 'authorization_code');
assert.equal(p.code, '4/abc');
return { body: { access_token: 'a', refresh_token: 'rt', expires_in: 3600 } };
});
const input = new PassThrough();
const output = new PassThrough();
let shown = '';
output.on('data', (d) => (shown += d));
input.end('4/abc\n');
const auth = new Auth({ env: { HOME: dir }, fetch });
await auth.login({ manual: true, input, output });
const url = new URL(shown.match(/https:\/\/accounts\.google\.com\S+/)[0]);
assert.equal(url.searchParams.get('code_challenge_method'), 'S256');
assert.equal(url.searchParams.get('token_usage'), 'remote');
assert.equal(
url.searchParams.get('redirect_uri'),
'https://sdk.cloud.google.com/applicationdefaultauthcode.html',
);
assert.match(url.searchParams.get('scope'), /auth\/colaboratory/);
assert.ok(url.searchParams.get('code_challenge'));
assert.ok(fetch.calls[0].params.code_verifier);
const saved = JSON.parse(await readFile(credsPath, 'utf8'));
assert.equal(saved.type, 'authorized_user');
assert.equal(saved.refresh_token, 'rt');
assert.equal((await stat(credsPath)).mode & 0o777, 0o600);
assert.equal(await auth.token(), 'a');
});
test('login refuses when COLAB_CREDENTIALS is set', async () => {
const auth = new Auth({
env: { COLAB_CREDENTIALS: '/nonexistent/x.json' },
fetch: mockFetch(ok),
});
await assert.rejects(auth.login(), /COLAB_CREDENTIALS/);
});
const tick = (ms = 20) => new Promise((r) => setTimeout(r, ms));
// drives the loopback callback the way a browser would
function browser(fetch, query) {
const seen = { urls: [], responses: [] };
const openBrowser = async (url) => {
seen.urls.push(url);
const auth = new URL(url);
const redirect = new URL(auth.searchParams.get('redirect_uri'));
const q = typeof query === 'function' ? query(auth) : query;
for (const [k, v] of Object.entries(q)) redirect.searchParams.set(k, v);
seen.responses.push(await fetch(redirect, auth));
};
return { openBrowser, seen };
}
const codeFlow = (auth) => ({ code: '4/loop', state: auth.searchParams.get('state') });
const rejectedBody = () => ({ status: 400, body: { error: 'invalid_grant' } });
const rt = () => ({ body: { access_token: 'a', refresh_token: 'rt', expires_in: 3600 } });
async function listening(port) {
return new Promise((resolve) => {
const s = createConnection(port, '127.0.0.1');
s.once('connect', () => (s.destroy(), resolve(true)));
s.once('error', () => resolve(false));
});
}
test('default login uses loopback PKCE with no pasted code', async () => {
const { dir, credsPath } = await home();
const fetch = mockFetch(rt);
const b = browser((u) => globalThis.fetch(u), codeFlow);
const output = new PassThrough();
let shown = '';
output.on('data', (d) => (shown += d));
const auth = new Auth({ env: { HOME: dir }, fetch });
await auth.login({ output, openBrowser: b.openBrowser });
const url = new URL(b.seen.urls[0]);
assert.ok(shown.includes(b.seen.urls[0]));
assert.match(url.searchParams.get('redirect_uri'), /^http:\/\/127\.0\.0\.1:\d+\/$/);
assert.equal(url.searchParams.get('code_challenge_method'), 'S256');
assert.equal(url.searchParams.has('token_usage'), false);
assert.ok(url.searchParams.get('state'));
const { params } = fetch.calls[0];
assert.equal(params.code, '4/loop');
assert.equal(params.redirect_uri, url.searchParams.get('redirect_uri'));
assert.equal(
createHash('sha256').update(params.code_verifier).digest('base64url'),
url.searchParams.get('code_challenge'),
);
assert.ok(!shown.includes('4/loop'));
assert.equal(b.seen.responses[0].status, 200);
const saved = JSON.parse(await readFile(credsPath, 'utf8'));
assert.equal(saved.refresh_token, 'rt');
assert.equal((await stat(credsPath)).mode & 0o777, 0o600);
assert.equal(await auth.token(), 'a');
assert.equal(await listening(Number(new URL(params.redirect_uri).port)), false);
});
test('bad callbacks never finish or exchange, a later good one does', async () => {
const { dir } = await home();
const fetch = mockFetch(rt);
const statuses = [];
const openBrowser = async (url) => {
const auth = new URL(url);
const good = new URL(auth.searchParams.get('redirect_uri'));
const state = auth.searchParams.get('state');
const hit = async (u, init) => statuses.push((await globalThis.fetch(u, init)).status);
await hit(`${good.origin}/?code=x&state=wrong`);
await hit(`${good.origin}/?code=x`);
await hit(`${good.origin}/cb?code=x&state=${state}`);
await hit(`${good.origin}/?code=x&state=${state}&state=${state}`);
await hit(`${good.origin}/?state=${state}`);
await hit(`${good.origin}/?code=x&state=${state}`, { method: 'POST', body: 'code=x' });
const evil = await new Promise((resolve) => {
const req = httpRequest(
{
port: good.port,
host: '127.0.0.1',
path: `/?code=x&state=${state}`,
headers: { host: 'evil.example' },
},
(res) => (res.resume(), resolve(res.statusCode)),
);
req.end();
});
statuses.push(evil);
assert.equal(fetch.calls.length, 0);
await hit(`${good.origin}/?code=ok&state=${state}`);
};
await new Auth({ env: { HOME: dir }, fetch }).login({ output: new PassThrough(), openBrowser });
assert.deepEqual(statuses, [400, 400, 404, 400, 400, 404, 404, 200]);
assert.equal(fetch.calls.length, 1);
assert.equal(fetch.calls[0].params.code, 'ok');
});
test('google denial cancels without exchange and keeps existing credentials', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath, { token: 'keep', expiry: future() });
const before = await readFile(credsPath, 'utf8');
const fetch = mockFetch(rt);
const b = browser(
(u) => globalThis.fetch(u),
(a) => ({
error: 'access_denied',
state: a.searchParams.get('state'),
}),
);
await assert.rejects(
new Auth({ env: { HOME: dir }, fetch }).login({
output: new PassThrough(),
openBrowser: b.openBrowser,
}),
/access_denied/,
);
assert.equal(fetch.calls.length, 0);
assert.equal(await readFile(credsPath, 'utf8'), before);
});
test('error with the wrong state is ignored', async () => {
const { dir } = await home();
const controller = new AbortController();
const b = browser(
async (u) => {
const res = await globalThis.fetch(u);
controller.abort();
return res;
},
{ error: 'access_denied', state: 'nope' },
);
await assert.rejects(
new Auth({ env: { HOME: dir }, fetch: mockFetch(rt) }).login({
output: new PassThrough(),
openBrowser: b.openBrowser,
signal: controller.signal,
}),
/cancelled/,
);
assert.equal(b.seen.responses[0].status, 400);
});
test('timeout and abort close the listener and keep credentials', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath);
const before = await readFile(credsPath, 'utf8');
for (const mode of ['timeout', 'abort']) {
const controller = new AbortController();
let port;
const openBrowser = (url) => {
port = Number(new URL(new URL(url).searchParams.get('redirect_uri')).port);
if (mode === 'abort') setTimeout(() => controller.abort(), 10);
};
const fetch = mockFetch(rt);
await assert.rejects(
new Auth({ env: { HOME: dir }, fetch }).login({
output: new PassThrough(),
openBrowser,
signal: controller.signal,
timeoutMs: mode === 'timeout' ? 30 : 5000,
}),
mode === 'timeout' ? /timed out/ : /cancelled/,
);
assert.equal(await listening(port), false);
assert.equal(fetch.calls.length, 0);
}
assert.equal(await readFile(credsPath, 'utf8'), before);
});
test('already-aborted signal never opens a listener or browser', async () => {
const controller = new AbortController();
controller.abort();
let opened = false;
await assert.rejects(
new Auth({ env: {}, fetch: mockFetch(rt) }).login({
output: new PassThrough(),
signal: controller.signal,
openBrowser: () => (opened = true),
}),
/cancelled/,
);
assert.equal(opened, false);
});
test('token exchange failure keeps existing credentials and closes the listener', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath, { token: 'keep', expiry: future() });
const before = await readFile(credsPath, 'utf8');
let port;
const b = browser((u) => globalThis.fetch(u), codeFlow);
const open = b.openBrowser;
await assert.rejects(
new Auth({ env: { HOME: dir }, fetch: mockFetch(rejectedBody) }).login({
output: new PassThrough(),
openBrowser: (url) => {
port = Number(new URL(new URL(url).searchParams.get('redirect_uri')).port);
return open(url);
},
}),
/invalid_grant/,
);
assert.equal(await listening(port), false);
assert.equal(await readFile(credsPath, 'utf8'), before);
const netFail = async () => {
throw new Error('boom');
};
await assert.rejects(
new Auth({ env: { HOME: dir }, fetch: netFail }).login({
output: new PassThrough(),
openBrowser: browser((u) => globalThis.fetch(u), codeFlow).openBrowser,
}),
/network error/,
);
assert.equal(await readFile(credsPath, 'utf8'), before);
});
test('browser open failure leaves the printed url usable', async () => {
const { dir } = await home();
const output = new PassThrough();
let shown = '';
output.on('data', (d) => (shown += d));
const good = browser((u) => globalThis.fetch(u), codeFlow);
await new Auth({ env: { HOME: dir }, fetch: mockFetch(rt) }).login({
output,
openBrowser: async (url) => {
await tick();
// user follows the printed url by hand
await good.openBrowser(url);
throw new Error('no xdg-open');
},
});
assert.match(shown, /could not open a browser/);
assert.match(shown, /https:\/\/accounts\.google\.com\S+/);
});
test('login refuses managed mode and leaves files alone', async () => {
const auth = new Auth({
env: {
COLAB_MANAGED_CREDENTIALS: '/nonexistent/m.json',
COLAB_LOGIN_URL: 'https://h/.auth/colab',
},
fetch: mockFetch(ok),
});
await assert.rejects(auth.login(), /MANAGED.*https:\/\/h\/\.auth\/colab/);
});
const writeManaged = (path, extra = {}) =>
writeCreds(path, { client_id: 'm-id', client_secret: 'm-secret', ...extra });
async function managed(extra = {}) {
const { dir } = await home();
const path = join(dir, 'managed.json');
const env = {
HOME: dir,
COLAB_MANAGED_CREDENTIALS: path,
COLAB_LOGIN_URL: 'https://h/.auth/colab',
...extra,
};
return { dir, path, env };
}
test('managed file is authoritative over local client overrides, never written', async () => {
const { path, env } = await managed({ COLAB_CLIENT_ID: 'env-id', COLAB_CLIENT_SECRET: 's' });
await writeManaged(path, { client_id: 'm-id', client_secret: 'm-secret' });
const before = await readFile(path, 'utf8');
const fetch = mockFetch(ok);
const auth = new Auth({ env, fetch });
assert.equal(await auth.token(), 'new-token');
assert.equal(fetch.calls[0].params.client_id, 'm-id');
assert.equal(fetch.calls[0].params.client_secret, 'm-secret');
assert.equal(await auth.token(), 'new-token');
assert.equal(fetch.calls.length, 1);
assert.equal(await readFile(path, 'utf8'), before);
});
test('absent managed file falls back only to explicit COLAB_CREDENTIALS', async () => {
const { dir, env } = await managed();
await assert.rejects(
new Auth({ env, fetch: mockFetch(ok) }).token(),
/no managed colab credentials.*https:\/\/h\/\.auth\/colab/,
);
const ext = join(dir, 'ext.json');
await writeCreds(ext, { token: 'ext-tok', expiry: future() });
await assert.rejects(
new Auth({ env, fetch: mockFetch(ok) }).token(),
/no managed/,
'local credentials are not an implicit fallback',
);
const auth = new Auth({ env: { ...env, COLAB_CREDENTIALS: ext }, fetch: mockFetch(ok) });
assert.equal(await auth.token(), 'ext-tok');
});
test('corrupt managed file fails closed even with an external fallback', async () => {
const { dir, path, env } = await managed();
const ext = join(dir, 'ext.json');
await writeCreds(ext, { token: 'ext-tok', expiry: future() });
await writeFile(path, '{nope');
const auth = new Auth({ env: { ...env, COLAB_CREDENTIALS: ext }, fetch: mockFetch(ok) });
await assert.rejects(auth.token(), /invalid json.*https:\/\/h\/\.auth\/colab/);
await writeFile(path, JSON.stringify({ type: 'authorized_user' }));
await assert.rejects(auth.token(), /no refresh_token/);
});
test('managed file without a client id/secret fails closed', async () => {
const { path, env } = await managed();
await writeCreds(path, { client_id: 'm-id' });
const fetch = mockFetch(ok);
await assert.rejects(new Auth({ env, fetch }).token(), /no client_id\/client_secret/);
assert.equal(fetch.calls.length, 0);
});
test('old refresh token failing after replacement is discarded, not thrown', async () => {
const { path, env } = await managed();
await writeManaged(path, { refresh_token: 'old-rt' });
let release;
const gate = new Promise((r) => (release = r));
const fetch = mockFetch(async (p) => {
if (p.refresh_token !== 'old-rt') return ok();
await gate;
return rejectedBody();
});
const auth = new Auth({ env, fetch });
const pending = auth.token();
await tick();
await writeManaged(path, { refresh_token: 'new-rt' });
release();
assert.equal(await pending, 'new-token');
assert.deepEqual(
fetch.calls.map((c) => c.params.refresh_token),
['old-rt', 'new-rt'],
);
});
test('replacing the managed file is picked up; unchanged files are not reloaded', async () => {
const { path, env } = await managed();
await writeManaged(path, { token: 'one', expiry: future() });
const fetch = mockFetch(ok);
const auth = new Auth({ env, fetch });
assert.equal(await auth.token(), 'one');
await writeManaged(path, { token: 'two', expiry: future() });
assert.equal(await auth.token(), 'two');
await rm(path);
await assert.rejects(auth.token(), /no managed/);
await writeManaged(path, { token: 'three', expiry: future() });
assert.equal(await auth.token(), 'three');
assert.equal(fetch.calls.length, 0);
});
test('managed file appearing later takes over from the external fallback', async () => {
const { dir, path, env } = await managed();
const ext = join(dir, 'ext.json');
await writeCreds(ext, { token: 'ext-tok', expiry: future() });
const auth = new Auth({ env: { ...env, COLAB_CREDENTIALS: ext }, fetch: mockFetch(ok) });
assert.equal(await auth.token(), 'ext-tok');
await writeManaged(path, { token: 'managed-tok', expiry: future() });
assert.equal(await auth.token(), 'managed-tok');
});
test('stale in-flight refresh cannot overwrite a newer credential generation', async () => {
const { path, env } = await managed();
await writeManaged(path, { refresh_token: 'old-rt' });
let release;
const gate = new Promise((r) => (release = r));
const fetch = mockFetch(async (p) => {
if (p.refresh_token === 'old-rt') {
await gate;
return { body: { access_token: 'stale-token', expires_in: 3600 } };
}
return { body: { access_token: 'fresh-token', expires_in: 3600 } };
});
const auth = new Auth({ env, fetch });
const stale = auth.token();
await tick();
await writeManaged(path, { refresh_token: 'new-rt' });
const newer = auth.token();
await tick();
release();
assert.equal(await newer, 'fresh-token');
assert.equal(await stale, 'fresh-token');
assert.equal(await auth.token(), 'fresh-token');
assert.deepEqual(fetch.calls.map((c) => c.params.refresh_token).sort(), ['new-rt', 'old-rt']);
});
test('local refresh does not clobber a file replaced mid-flight', async () => {
const { dir, credsPath } = await home();
await writeCreds(credsPath, { refresh_token: 'old-rt' });
let release;
const gate = new Promise((r) => (release = r));
const fetch = mockFetch(async () => {
await gate;
return ok();
});
const auth = new Auth({ env: { HOME: dir }, fetch });
const pending = auth.token();
await tick();
await writeCreds(credsPath, { refresh_token: 'new-rt', token: 'new-file', expiry: future() });
release();
assert.equal(await pending, 'new-file');
assert.equal(JSON.parse(await readFile(credsPath, 'utf8')).refresh_token, 'new-rt');
});