import axios from "axios"; import { withPrefix } from "@/utils/redirect"; import { rethrowSimple } from "@/utils/simple-error"; export type OIDCConfigEntry = { icon?: string; label?: string; custom_button_text?: string; end_user_registration_endpoint?: string; profile_url?: string; }; /** Shape of the OIDC config object coming from Galaxy’s `/api/config`. */ export type OIDCConfig = Record; export type OIDCConfigWithRegistration = Record; /** Return the per-IDP config, minus anything the caller wants to hide. */ export function getFilteredOIDCIdps(oidcConfig: OIDCConfig, exclude: string[] = []): OIDCConfig { const blacklist = new Set(["cilogon", ...exclude]); const filtered: OIDCConfig = {}; Object.entries(oidcConfig).forEach(([idp, cfg]) => { if (!blacklist.has(idp)) { filtered[idp] = cfg; } }); return filtered; } export function getOIDCIdpsWithRegistration(oidcConfig: OIDCConfig): OIDCConfigWithRegistration { const filtered: OIDCConfigWithRegistration = {}; Object.entries(oidcConfig).forEach(([idp, cfg]) => { if (cfg.end_user_registration_endpoint && typeof cfg.end_user_registration_endpoint === "string") { filtered[idp] = { ...cfg, end_user_registration_endpoint: cfg.end_user_registration_endpoint, }; } }); return filtered; } /** Do we need to show the institution picker at all? */ export const getNeedShowCilogonInstitutionList = (cfg: OIDCConfig): boolean => { return Boolean(cfg.cilogon); }; /** * Generic OIDC login (all providers *except* CILogon). * Returns the redirect URI Galaxy gives back, or throws. */ export async function submitOIDCLogon(idp: string, redirectParam: string | null = null): Promise { const formData = new FormData(); formData.append("next", redirectParam ?? ""); try { const { data } = await axios.post<{ redirect_uri?: string }>(withPrefix(`/authnz/${idp}/login`), formData, { withCredentials: true, }); return data.redirect_uri ?? null; } catch (error) { rethrowSimple(error); } } /** * CILogon login. * @param useIDPHint If true, append ?idphint= * @param idpHint The entityID to hint with (ignored when useIDPHint = false) */ export async function submitCILogon(useIDPHint = false, idpHint?: string): Promise { let url = withPrefix("/authnz/cilogon/login/"); if (useIDPHint && idpHint) { url += `?idphint=${encodeURIComponent(idpHint)}`; } try { const { data } = await axios.post<{ redirect_uri?: string }>(url); return data.redirect_uri ?? null; } catch (error) { rethrowSimple(error); } } export function isOnlyOneOIDCProviderConfigured(config: OIDCConfig): boolean { return Object.keys(config).length === 1; } export function getSingleOidcConfig(config: OIDCConfig): OIDCConfigEntry | null { const providers = Object.keys(config); if (providers.length !== 1) { return null; } const idp = providers[0]; if (idp === undefined) { throw new Error("OIDC provider key is undefined."); } return config[idp] || null; } export function hasSingleOidcProfile(config: OIDCConfig): boolean { if (!isOnlyOneOIDCProviderConfigured(config)) { return false; } const idp_config = getSingleOidcConfig(config); return !!idp_config?.profile_url; } export async function redirectToSingleProvider(config: OIDCConfig): Promise { const providers = Object.keys(config); if (providers.length !== 1) { return null; } const idp = providers[0]; if (!idp) { throw new Error("OIDC provider key is undefined."); } if (idp === "cilogon") { const redirectUri = await submitCILogon(false); return redirectUri; } else { const redirectUri = await submitOIDCLogon(idp, ""); return redirectUri; } }