fix(auth): harden delayed oauth token registration

This commit is contained in:
Tam Nhu Tran committed 2026-04-04 00:37:04 -04:00
1 parent e2e5d99465
commit 01313a5a8e
4 files changed
+457 -36

No files matched your search

@@ -173,13 +173,13 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
expect(registry.providers.kiro.accounts['github-ABC123']?.nickname).toBe('work');
});
it('returns 409 when callback completes upstream but no account can be registered locally', async () => {
it('returns wait after callback submission when the local token is not yet available', async () => {
mockFetch([
{
url: /\/v0\/management\/kiro-auth-url\?is_webui=true&method=google$/,
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
response: {
auth_url: 'https://auth.example.com/authorize?state=state-409',
state: 'state-409',
auth_url: 'https://auth.example.com/authorize?state=state-callback-wait',
state: 'state-callback-wait',
},
},
{
@@ -187,23 +187,41 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
method: 'POST',
response: { status: 'ok' },
},
{
url: /\/v0\/management\/get-auth-status\?state=state-callback-wait$/,
response: { status: 'ok' },
},
]);
const startResponse = await postJson('/api/cliproxy/auth/kiro/start-url', {
nickname: 'work',
kiroMethod: 'google',
});
const startResponse = await postJson('/api/cliproxy/auth/codex/start-url', {});
expect(startResponse.status).toBe(200);
const callbackResponse = await postJson('/api/cliproxy/auth/kiro/submit-callback', {
redirectUrl: 'http://localhost/callback?code=abc123&state=state-409',
});
const realDateNow = Date.now;
let now = realDateNow();
Date.now = () => now;
try {
const callbackResponse = await postJson('/api/cliproxy/auth/codex/submit-callback', {
redirectUrl: 'http://localhost/callback?code=abc123&state=state-callback-wait',
});
expect(callbackResponse.status).toBe(409);
expect(callbackResponse.body).toEqual({
error:
'Authenticated token could not be matched to a new account. Retry the flow and choose a different nickname if needed.',
});
expect(callbackResponse.status).toBe(200);
expect(callbackResponse.body).toEqual({ status: 'wait' });
now += 16_000;
const statusResponse = await getJson(
'/api/cliproxy/auth/codex/status?state=state-callback-wait'
);
expect(statusResponse.status).toBe(409);
expect(statusResponse.body).toEqual({
status: 'error',
error:
'Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.',
});
} finally {
Date.now = realDateNow;
}
});
it('keeps polling briefly after upstream completion before surfacing the missing-token error', async () => {
@@ -310,6 +328,191 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
});
});
it('lets polling finish successfully after callback submission once the token appears', async () => {
mockFetch([
{
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
response: {
auth_url: 'https://auth.example.com/authorize?state=state-callback-delayed',
state: 'state-callback-delayed',
},
},
{
url: /\/v0\/management\/oauth-callback$/,
method: 'POST',
response: { status: 'ok' },
},
{
url: /\/v0\/management\/get-auth-status\?state=state-callback-delayed$/,
response: { status: 'ok' },
},
]);
const startResponse = await postJson('/api/cliproxy/auth/codex/start-url', {});
expect(startResponse.status).toBe(200);
const callbackResponse = await postJson('/api/cliproxy/auth/codex/submit-callback', {
redirectUrl: 'http://localhost/callback?code=abc123&state=state-callback-delayed',
});
expect(callbackResponse.status).toBe(200);
expect(callbackResponse.body).toEqual({ status: 'wait' });
const tokenDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(tokenDir, { recursive: true });
fs.writeFileSync(
path.join(tokenDir, 'codex-callback@example.com.json'),
JSON.stringify({ type: 'codex', email: 'callback@example.com' }),
'utf8'
);
const statusResponse = await getJson(
'/api/cliproxy/auth/codex/status?state=state-callback-delayed'
);
expect(statusResponse.status).toBe(200);
expect(statusResponse.body).toEqual({
status: 'ok',
account: {
id: 'callback@example.com',
email: 'callback@example.com',
nickname: 'callback',
provider: 'codex',
isDefault: true,
},
});
});
it('keeps the pending auth state alive through the local-token grace window even near TTL expiry', async () => {
mockFetch([
{
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
response: {
auth_url: 'https://auth.example.com/authorize?state=state-ttl-grace',
state: 'state-ttl-grace',
},
},
{
url: /\/v0\/management\/get-auth-status\?state=state-ttl-grace$/,
response: { status: 'ok' },
},
{
url: /\/v0\/management\/get-auth-status\?state=state-ttl-grace$/,
response: { status: 'ok' },
},
]);
const realDateNow = Date.now;
let now = realDateNow();
Date.now = () => now;
try {
const startResponse = await postJson('/api/cliproxy/auth/codex/start-url', {});
expect(startResponse.status).toBe(200);
now += 10 * 60 * 1000 - 1_000;
const firstStatusResponse = await getJson(
'/api/cliproxy/auth/codex/status?state=state-ttl-grace'
);
expect(firstStatusResponse.status).toBe(200);
expect(firstStatusResponse.body).toEqual({ status: 'wait' });
const tokenDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(tokenDir, { recursive: true });
fs.writeFileSync(
path.join(tokenDir, 'codex-ttl@example.com.json'),
JSON.stringify({ type: 'codex', email: 'ttl@example.com' }),
'utf8'
);
now += 2_000;
const secondStatusResponse = await getJson(
'/api/cliproxy/auth/codex/status?state=state-ttl-grace'
);
expect(secondStatusResponse.status).toBe(200);
expect(secondStatusResponse.body).toEqual({
status: 'ok',
account: {
id: 'ttl@example.com',
email: 'ttl@example.com',
nickname: 'ttl',
provider: 'codex',
isDefault: true,
},
});
} finally {
Date.now = realDateNow;
}
});
it('does not treat rewrites of pre-existing token files as a newly added account', async () => {
const tokenDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(tokenDir, { recursive: true });
const tokenPath = path.join(tokenDir, 'codex-existing@example.com.json');
fs.writeFileSync(
tokenPath,
JSON.stringify({ type: 'codex', email: 'existing@example.com', version: 1 }),
'utf8'
);
mockFetch([
{
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
response: {
auth_url: 'https://auth.example.com/authorize?state=state-existing-rewrite',
state: 'state-existing-rewrite',
},
},
{
url: /\/v0\/management\/get-auth-status\?state=state-existing-rewrite$/,
response: { status: 'ok' },
},
{
url: /\/v0\/management\/get-auth-status\?state=state-existing-rewrite$/,
response: { status: 'ok' },
},
]);
const startResponse = await postJson('/api/cliproxy/auth/codex/start-url', {});
expect(startResponse.status).toBe(200);
fs.writeFileSync(
tokenPath,
JSON.stringify({ type: 'codex', email: 'existing@example.com', version: 2 }),
'utf8'
);
const realDateNow = Date.now;
let now = realDateNow();
Date.now = () => now;
try {
const firstStatusResponse = await getJson(
'/api/cliproxy/auth/codex/status?state=state-existing-rewrite'
);
expect(firstStatusResponse.status).toBe(200);
expect(firstStatusResponse.body).toEqual({ status: 'wait' });
now += 16_000;
const secondStatusResponse = await getJson(
'/api/cliproxy/auth/codex/status?state=state-existing-rewrite'
);
expect(secondStatusResponse.status).toBe(409);
expect(secondStatusResponse.body).toEqual({
status: 'error',
error:
'Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.',
});
} finally {
Date.now = realDateNow;
}
});
it('registers the new account before reporting polled auth success', async () => {
mockFetch([
{