Files
prowler/ui/lib/auth-callback-url.ts
T

125 lines
3.5 KiB
TypeScript

import { extractUtmParams, type UtmParams } from "@/lib/utm";
const DEFAULT_CALLBACK_PATH = "/";
const INVITATION_TOKEN_PARAM = "invitation_token";
// Origin used only to resolve relative paths; never part of the returned value.
const INTERNAL_ORIGIN = "http://localhost";
type CallbackSearchParams = {
get(name: string): string | null;
};
export const getSafeCallbackPathFromValue = (
value: string | null | undefined,
) => {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
return DEFAULT_CALLBACK_PATH;
}
// A prefix check is not enough: the URL parser normalizes backslashes and
// control characters, so "/\evil.com" or "/\t/evil.com" pass the check above
// yet resolve to an external origin. Resolve against a fixed origin and
// confirm it stayed internal before trusting the path.
try {
const url = new URL(value, INTERNAL_ORIGIN);
if (url.origin !== INTERNAL_ORIGIN) {
return DEFAULT_CALLBACK_PATH;
}
return `${url.pathname}${url.search}${url.hash}`;
} catch (_error) {
return DEFAULT_CALLBACK_PATH;
}
};
export const getSafeCallbackPath = (
searchParams: CallbackSearchParams,
key = "state",
) => getSafeCallbackPathFromValue(searchParams.get(key));
export const appendCallbackState = (authUrl: string, callbackPath: string) => {
const safeCallbackPath = getSafeCallbackPathFromValue(callbackPath);
if (safeCallbackPath === DEFAULT_CALLBACK_PATH) {
return authUrl;
}
try {
const url = new URL(authUrl);
url.searchParams.set("state", safeCallbackPath);
return url.toString();
} catch (_error) {
return authUrl;
}
};
export const getInvitationTokenFromCallbackPath = (callbackPath: string) => {
const safeCallbackPath = getSafeCallbackPathFromValue(callbackPath);
if (safeCallbackPath === DEFAULT_CALLBACK_PATH) {
return null;
}
try {
const url = new URL(safeCallbackPath, "http://localhost");
return url.searchParams.get(INVITATION_TOKEN_PARAM);
} catch (_error) {
return null;
}
};
export const appendAttributionToCallbackPath = (
callbackPath: string,
attribution: UtmParams,
): string => {
const safeCallbackPath = getSafeCallbackPathFromValue(callbackPath);
if (Object.keys(attribution).length === 0) {
return safeCallbackPath;
}
try {
const url = new URL(safeCallbackPath, INTERNAL_ORIGIN);
for (const [key, value] of Object.entries(attribution)) {
if (!url.searchParams.has(key)) {
url.searchParams.set(key, value);
}
}
return `${url.pathname}${url.search}${url.hash}`;
} catch (_error) {
return safeCallbackPath;
}
};
export const getAttributionParamsFromCallbackPath = (
callbackPath: string,
): UtmParams => {
const safeCallbackPath = getSafeCallbackPathFromValue(callbackPath);
try {
const url = new URL(safeCallbackPath, INTERNAL_ORIGIN);
return extractUtmParams(url.searchParams);
} catch (_error) {
return {};
}
};
const SELF_REGISTRATION_DISABLED_CODE = "self_registration_disabled";
// The API answers a social login from a brand-new user with this error code
// when the deployment only allows invited users.
export const isSelfRegistrationDisabledResponse = async (
response: Response,
): Promise<boolean> => {
if (response.status !== 403) return false;
try {
const body = (await response.json()) as {
errors?: Array<{ code?: string }>;
};
return (
body.errors?.some(
(error) => error.code === SELF_REGISTRATION_DISABLED_CODE,
) ?? false
);
} catch (_error) {
return false;
}
};