feat: ( redirect ) add redirect url based on the domain name

This commit is contained in:
Abubeker Yasin
2026-07-14 14:16:48 +03:00
parent 1ace808b4a
commit ef5d643ba4
8 changed files with 179 additions and 11 deletions

View File

@@ -0,0 +1,82 @@
/**
* Domain-aware redirect helpers.
*
* The passenger portal is served from more than one public domain
* (e.g. https://bookingedr.et and https://passenger.edrsc.com). Payment
* providers and Fayda need a browser-facing return/redirect URL, and that URL
* must live on the SAME domain the user is currently browsing — otherwise the
* user is bounced to the "other" site mid-flow.
*
* We derive the target domain from the incoming request's Origin (falling back
* to Referer), but only trust it when it matches an explicit allowlist
* (PAYMENT_REDIRECT_ALLOWED_ORIGINS) so a spoofed Origin can't turn these into
* an open redirect. When the origin is absent or not allowlisted, the
* configured (env) URL is left untouched.
*
* NOTE: this only rebases the ORIGIN (scheme + host + port). The path/query of
* the configured URL is preserved, so the redirect always keeps its known
* suffix (e.g. /booking/payment/telebirr/success). For Fayda this matters — the
* full redirect_uri, host included, must be pre-registered with eSignet for
* BOTH domains, or the login is rejected.
*/
/** Normalize to a bare origin ("https://host[:port]"), or null if unparseable. */
function toOrigin(value?: string | null): string | null {
if (!value) return null;
try {
return new URL(value.trim()).origin;
} catch {
return null;
}
}
/** Allowlisted origins from PAYMENT_REDIRECT_ALLOWED_ORIGINS (comma-separated). */
function allowedOrigins(): string[] {
return (process.env.PAYMENT_REDIRECT_ALLOWED_ORIGINS ?? "")
.split(",")
.map((o) => toOrigin(o))
.filter((o): o is string => o !== null);
}
/**
* Pick the origin the request came from, restricted to the allowlist. Sources
* are tried in order of trustworthiness:
* 1. `origin` — the Origin header (most reliable on cross-origin requests).
* 2. `referer` — Referer fallback (browsers omit Origin on some same-site
* navigations but usually still send Referer).
* 3. `frontendBaseUrl` — the X-Frontend-Base-URL header the portal sets
* explicitly, used as a last resort when neither of the above is present.
* Returns null when none is present or allowlisted — callers then keep the
* configured URL.
*/
export function resolveAllowedOrigin(
origin?: string | null,
referer?: string | null,
frontendBaseUrl?: string | null,
): string | null {
const candidate =
toOrigin(origin) ?? toOrigin(referer) ?? toOrigin(frontendBaseUrl);
if (!candidate) return null;
return allowedOrigins().includes(candidate) ? candidate : null;
}
/**
* Return `configuredUrl` with its origin swapped to `allowedOrigin`, preserving
* path, query and hash. If `allowedOrigin` is null or either URL is unparseable,
* the configured URL is returned unchanged.
*/
export function rebaseUrlOrigin(
configuredUrl: string | undefined,
allowedOrigin: string | null,
): string | undefined {
if (!configuredUrl || !allowedOrigin) return configuredUrl;
try {
const target = new URL(configuredUrl);
const base = new URL(allowedOrigin);
target.protocol = base.protocol;
target.host = base.host;
return target.toString();
} catch {
return configuredUrl;
}
}