fix: alert errors from timeouts (#4066)

This commit is contained in:
Daniel Salazar authored and GitHub committed 2026-10-04 02:44:40 -07:00
1 parent f533eb84ae
commit 51eba174b3
3 files changed
+77 -15

No files matched your search

@@ -701,6 +701,26 @@ describe('OIDCController login callback', () => {
expect(captured.redirectUrl).toContain('auth_error=1');
});
it('redirects with auth_error when the provider is unreachable', async () => {
const state = oidc().signState({
provider: 'custom',
redirect_uri: TEST_ORIGIN + '/',
});
vi.spyOn(oidc(), 'exchangeCodeForTokens').mockRejectedValue(
new TypeError('fetch failed'),
);
const { res, captured } = makeRes();
await callRoute(
'get',
'/auth/oidc/callback/login',
makeReq({ query: { code: 'c', state } }),
res,
);
expect(captured.redirectStatus).toBe(302);
expect(captured.redirectUrl).toContain('auth_error=1');
});
it('parses code/state from the POST body (Apple form_post)', async () => {
const state = oidc().signState({
provider: 'custom',
@@ -1726,6 +1746,30 @@ describe('OIDCController revalidate callback', () => {
expect(String(captured.body)).toContain('Missing');
});
it('returns 400 when the userinfo request cannot reach the provider', async () => {
const state = oidc().signState({
provider: 'custom',
flow: 'revalidate',
user_uuid: uuidv4(),
});
vi.spyOn(oidc(), 'exchangeCodeForTokens').mockResolvedValue({
access_token: 'access',
} as never);
vi.spyOn(oidc(), 'getUserInfo').mockRejectedValue(
new TypeError('fetch failed'),
);
const { res, captured } = makeRes();
await callRoute(
'get',
'/auth/oidc/callback/revalidate',
makeReq({ query: { code: 'c', state } }),
res,
);
expect(captured.statusCode).toBe(400);
expect(String(captured.body)).toContain('Please try again');
});
it('returns 403 when the OIDC sub resolves to a different account than the session', async () => {
// The linked account exists, but state.user_uuid points at someone else.
const sub = `sub-${Math.random().toString(36).slice(2, 8)}`;
+25 -15
View File
@@ -963,23 +963,33 @@ if (window.opener) {
const callbackUrl = this.services.oidc.getCallbackUrl(flow);
if (!callbackUrl) return { error: 'Invalid flow.' };
const tokens = await this.services.oidc.exchangeCodeForTokens(
provider,
code,
callbackUrl,
);
if (!tokens || !tokens.access_token)
return { error: 'Token exchange failed.' };
try {
const tokens = await this.services.oidc.exchangeCodeForTokens(
provider,
code,
callbackUrl,
);
if (!tokens || !tokens.access_token)
return { error: 'Token exchange failed.' };
const userinfo = await this.services.oidc.getUserInfo(
provider,
tokens.access_token,
typeof tokens.id_token === 'string' ? tokens.id_token : undefined,
);
if (!userinfo || !userinfo.sub)
return { error: 'Could not get user info.' };
const userinfo = await this.services.oidc.getUserInfo(
provider,
tokens.access_token,
typeof tokens.id_token === 'string'
? tokens.id_token
: undefined,
);
if (!userinfo || !userinfo.sub)
return { error: 'Could not get user info.' };
return { provider, userinfo, stateDecoded };
return { provider, userinfo, stateDecoded };
} catch (e) {
// Network failures reaching the provider; the user can retry.
console.warn(`[oidc] ${provider} request failed`, e);
return {
error: 'Could not reach the sign-in provider. Please try again.',
};
}
}
async #finishLogin(
+8
View File
@@ -18,6 +18,7 @@
*/
import { isSpanContextValid, trace } from '@opentelemetry/api';
import { setDefaultAutoSelectFamilyAttemptTimeout } from 'node:net';
import { puterClients } from './clients';
import { loadConfig } from './config';
import { puterControllers } from './controllers';
@@ -32,8 +33,15 @@ import { installJsonConsole } from './util/jsonConsole.js';
// it stops accepting connections.
const GRACEFUL_DRAIN_MS = 90_000;
// Node's happy-eyeballs default (250 ms) abandons a slow IPv4 handshake and
// falls through to IPv6; on hosts with no IPv6 route, one dropped SYN fails
// the whole connect. 2 s outlasts a single SYN retransmit.
const CONNECT_ATTEMPT_TIMEOUT_MS = 2_000;
// if called directly, start the server
if (require.main === module) {
setDefaultAutoSelectFamilyAttemptTimeout(CONNECT_ATTEMPT_TIMEOUT_MS);
const config = loadConfig();
// Structured logging: when `log_format: "json"`, replace the global console