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

  1. Send returnUrl as an absolute URL matching the server's configured canonical origin
  2. Fix serverURL / trust-proxy configuration so the allowlist includes the host the browser is actually on
  3. Ensure the reverse proxy forwards correct X-Forwarded-Proto/Host headers so the server sees https
  4. 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

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.