actualbudget/actual · error

Invalid redirect URL

Invalid redirect URL

Error message

Invalid redirect URL

What it means

After a successful OpenID finalize, the server validates the redirect target with isValidRedirectUrl before issuing res.redirect. If the resulting URL is not a valid/allowed redirect (typically not relative to the server's own origin or absent), it responds 400 with reason 'Invalid redirect URL'. This guards against open-redirect attacks via crafted state or configuration.

Source

Thrown at packages/sync-server/src/app-openid.ts:108

    const openIdConfig = JSON.parse(auth.extra_data);
    res.send({ status: 'ok', data: { openId: openIdConfig } });
  } catch {
    res
      .status(500)
      .send({ status: 'error', reason: 'Invalid OpenID configuration' });
  }
});

app.get('/callback', async (req, res) => {
  const { error, url } = await loginWithOpenIdFinalize(req.query);

  if (error) {
    res.status(400).send({ status: 'error', reason: error });
    return;
  }

  if (!isValidRedirectUrl(url)) {
    res.status(400).send({ status: 'error', reason: 'Invalid redirect URL' });
    return;
  }

  res.redirect(url);
});

app.use(errorMiddleware);

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Ensure the server's canonical URL/base origin configuration matches the URL users actually browse to
  2. Configure the reverse proxy to pass X-Forwarded-Host/X-Forwarded-Proto so the computed redirect matches the requested origin
  3. Start the login flow again from the app UI instead of a bookmarked/hand-built callback URL
  4. Inspect loginWithOpenIdFinalize/isValidRedirectUrl in app-openid.ts to confirm which origin is considered valid

Example fix

// before: server behind proxy computes http://internal:5006/redirect
// after: pass forwarded headers
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
Defensive patterns

Strategy: validation

Validate before calling

function willRedirectValidate(url, serverOrigin) {
  try {
    const u = new URL(url, serverOrigin);
    return u.origin === new URL(serverOrigin).origin;
  } catch { return false; }
}
// ensure the browser origin matches the server's canonical URL before logging in

Type guard

function isSameOriginRedirect(url, origin) {
  try { return new URL(url, origin).origin === new URL(origin).origin; }
  catch { return false; }
}

Try / catch

if (res.status === 400) {
  const body = await res.json();
  if (body.reason === 'Invalid redirect URL') {
    // recompute origin: open the app via the configured canonical URL and retry
    window.location.href = canonicalServerUrl + '/openid/login';
  }
}

Prevention

When it happens

Trigger: GET /openid/callback where the finalized `url` is undefined/null, an absolute URL to a foreign origin, or fails isValidRedirectUrl (e.g. missing the server's configured webhook/base origin).

Common situations: Server accessed via a different host/port/protocol than the configured canonical URL (reverse proxy without forwarded headers); state tampering; missing or wrong actual server URL configuration so the computed redirect points elsewhere.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/9de508a934db1e91. Report an issue: GitHub.