actualbudget/actual · error
Invalid redirect URL
Error message
Invalid redirect URL
What it means
The /login (openid) path of app-account.js rejects the request with 400 'Invalid redirect URL' when req.body.returnUrl fails isValidRedirectUrl(). The server only accepts return URLs pointing back to trusted origins (typically the server's own canonical URL), preventing open-redirect attacks during the OpenID login flow.
Source
Thrown at packages/sync-server/src/app-account.js:100
'*'.repeat(headerVal.length) || 'No password provided.';
console.debug('HEADER VALUE: ' + obfuscated);
if (headerVal === '') {
res.send({ status: 'error', reason: 'invalid-header' });
return;
} else {
if (validateAuthHeader(req)) {
tokenRes = await loginWithPassword(headerVal);
} else {
res.send({ status: 'error', reason: 'proxy-not-trusted' });
return;
}
}
break;
}
case 'openid': {
if (!isValidRedirectUrl(req.body.returnUrl)) {
res
.status(400)
.send({ status: 'error', reason: 'Invalid redirect URL' });
return;
}
const { error, url } = await loginWithOpenIdSetup(
req.body.returnUrl,
req.body.password,
);
if (error) {
res.status(400).send({ status: 'error', reason: error });
return;
}
res.send({ status: 'ok', data: { returnUrl: url } });
return;
}
default:
tokenRes = await loginWithPassword(req.body.password);View on GitHub (pinned to d4334cb6e6)
Solutions
- Send returnUrl as an absolute URL matching the server's configured canonical origin
- Fix serverURL / trust-proxy configuration so the allowlist includes the host the browser is actually on
- Ensure the reverse proxy forwards correct X-Forwarded-Proto/Host headers so the server sees https
- Align http/https between the configured server URL and the returnUrl
Example fix
// before
await request('/login', { method: 'POST', body: { loginMethod: 'openid', returnUrl: '/app' } });
// 400 Invalid redirect URL
// after
const returnUrl = new URL('/app', location.origin).href; // absolute, same origin
await request('/login', { method: 'POST', body: { loginMethod: 'openid', returnUrl } }); Defensive patterns
Strategy: validation
Validate before calling
function isValidClientReturnUrl(url) {
try {
const u = new URL(url, location.origin);
return u.origin === location.origin && (u.protocol === 'https:' || u.hostname === 'localhost');
} catch { return false; }
}
// assert before POST /login: isValidClientReturnUrl(returnUrl) Type guard
function isInvalidRedirectResponse(body) {
return body?.status === 'error' && body?.reason === 'Invalid redirect URL';
} Try / catch
const res = await request('/login', { method: 'POST', body: { loginMethod: 'openid', returnUrl } });
if (res.status === 400 && res.reason === 'Invalid redirect URL') {
returnUrl = new URL(returnUrl, serverUrl).href; // rebuild absolute, retry
} Prevention
- Always send absolute return URLs on the server's canonical origin
- Keep serverURL and reverse-proxy host/proto headers consistent
- Avoid relative paths; build URLs with new URL()
When it happens
Trigger: POST to the login endpoint with loginMethod 'openid' and a returnUrl that is absent, not absolute, uses an unexpected protocol, or points to a host other than the server's configured URL.
Common situations: Reverse-proxy on a different domain than the configured server URL; missing/wrong serverURL config so the allowlist does not match; client sending a relative path like '/gocardless' without origin; http vs https mismatch behind a TLS proxy.
Related errors
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/6b7da239a00e332f.
Report an issue: GitHub.