/* eslint-disable @next/next/no-img-element */ import { useEffect, useRef, useState } from 'react'; import getRawBody from 'raw-body'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; import type { OIDCSSORecord, ProductConfig, SAMLSSORecord } from '@boxyhq/saml-jackson'; import type { InferGetServerSidePropsType } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import jackson from '@lib/jackson'; import Head from 'next/head'; import { hexToOklch } from '@lib/color'; import { PoweredBy } from '@components/PoweredBy'; import { getPortalBranding, getProductBranding } from '@ee/branding/utils'; import { boxyhqHosted } from '@lib/env'; interface Connection { name: string; product: string; clientID: string; sortOrder: number | null; deactivated: boolean; } export default function ChooseIdPConnection({ connections, SAMLResponse, authFlow, branding, }: InferGetServerSidePropsType) { const { t } = useTranslation('common'); const primaryColor = hexToOklch(branding.primaryColor); const title = authFlow === 'sp-initiated' ? t('select_an_idp') : t('select_an_app'); const selectors = { 'sp-initiated': , 'idp-initiated': , }; return (
{`${title} - ${branding.companyName}`} {branding?.faviconUrl && } {primaryColor && } {branding?.logoUrl && (
{branding.companyName}
)} {authFlow in selectors ? ( selectors[authFlow] ) : (

{t('invalid_request_try_again')}

)}
); } const IdpSelector = ({ connections }: { connections: Connection[] }) => { const router = useRouter(); const { t } = useTranslation('common'); // SP initiated SSO: Redirect to the same path with idp_hint set to the selected connection clientID const connectionSelected = (clientID: string) => { return router.push(`${router.asPath}&idp_hint=${clientID}`); }; return ( <>

{t('select_an_idp')}

    {connections.map((connection) => { return (
  • ); })}

{t('choose_an_identity_provider_to_continue')}

); }; const AppSelector = ({ connections, SAMLResponse, }: { connections: Connection[]; SAMLResponse: string | null; }) => { const { t } = useTranslation('common'); const formRef = useRef(null); const [connection, setConnection] = useState(null); // Warn the user if they refresh the page useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (!connection) { e.preventDefault(); e.returnValue = ''; return ''; } }; if (!connection) { window.addEventListener('beforeunload', handleBeforeUnload); } return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [connection]); // IdP initiated SSO: Submit the SAMLResponse and idp_hint to the SAML ACS endpoint const appSelected = (e: React.ChangeEvent) => { setConnection(e.target.value); formRef.current?.submit(); }; if (!SAMLResponse) { return

{t('no_saml_response_try_again')}

; } return ( <>

{t('select_an_app')}

    {connections.map((connection) => { return (
  • ); })}

{t('choose_an_app_to_continue')}

); }; export const getServerSideProps = async ({ query, locale, req }) => { const { connectionAPIController, samlFederatedController, checkLicense, productController } = await jackson(); const paramsToRelay = { ...query } as { [key: string]: string }; const { authFlow, entityId, tenant, product, idp_hint, samlFedAppId, fedType } = query as { authFlow: 'sp-initiated' | 'idp-initiated'; tenant?: string; product?: string; idp_hint?: string; entityId?: string; samlFedAppId?: string; fedType?: string; }; if (!['sp-initiated', 'idp-initiated'].includes(authFlow)) { return { notFound: true, }; } // The user has selected an IdP to continue with if (idp_hint) { const params = new URLSearchParams(paramsToRelay); const destination = samlFedAppId && fedType !== 'oidc' ? `/api/federated-saml/sso?${params}` : `/api/oauth/authorize?${params}`; return { redirect: { destination, permanent: false, }, }; } // SAML federated app const samlFederationApp = samlFedAppId ? await samlFederatedController.app.get({ id: samlFedAppId }) : null; if (samlFedAppId && !samlFederationApp) { return { notFound: true, }; } // Otherwise, show the list of IdPs let connections: (OIDCSSORecord | SAMLSSORecord)[] = []; if (samlFederationApp) { const tenants = samlFederationApp?.tenants || [samlFederationApp.tenant]; const { product } = samlFederationApp; connections = await connectionAPIController.getConnections({ tenant: tenants, product, sort: true }); } else if (tenant && product) { connections = await connectionAPIController.getConnections({ tenant, product, sort: true }); } else if (entityId) { connections = await connectionAPIController.getConnections({ entityId: decodeURIComponent(entityId) }); } // Get the branding to use for the IdP selector screen let branding = boxyhqHosted && product ? await getProductBranding(product) : await getPortalBranding(); // For SAML federated requests, use the branding from the SAML federated app if (samlFederationApp && (await checkLicense())) { branding = { logoUrl: samlFederationApp?.logoUrl || branding.logoUrl, primaryColor: samlFederationApp?.primaryColor || branding.primaryColor, faviconUrl: samlFederationApp?.faviconUrl || branding.faviconUrl, companyName: samlFederationApp?.name || branding.companyName, }; } let connectionsTransformed: Connection[] = connections.map((connection) => { const idpMetadata = 'idpMetadata' in connection ? connection.idpMetadata : undefined; const oidcProvider = 'oidcProvider' in connection ? connection.oidcProvider : undefined; const name = connection.name || (idpMetadata ? idpMetadata.friendlyProviderName || idpMetadata.provider : `${oidcProvider?.provider}`); return { name, product: connection.product, clientID: connection.clientID, sortOrder: connection.sortOrder || null, deactivated: connection.deactivated || false, }; }); // Filter out connections that are not enabled connectionsTransformed = connectionsTransformed.filter((connection) => connection.deactivated !== true); // For idp-initiated flows, we need to parse the SAMLResponse from the request body and pass it to the component if (req.method == 'POST') { const body = await getRawBody(req); const params = new URLSearchParams(body.toString('utf-8')); const SAMLResponse = params.get('SAMLResponse'); // SAMLResponse should exist with idp-initiated flow if (!SAMLResponse) { return { notFound: true, }; } if (boxyhqHosted) { // Fetch products to display the product name instead of the product ID const products = (await Promise.allSettled( connectionsTransformed.map((connection) => productController.get(connection.product)) )) as PromiseFulfilledResult[]; connectionsTransformed = connectionsTransformed.map((connection, index) => { if (products[index].status === 'fulfilled') { return { ...connection, product: products[index].value.name || connection.product, }; } return connection; }); } return { props: { ...(locale ? await serverSideTranslations(locale, ['common']) : {}), authFlow, SAMLResponse, connections: connectionsTransformed, branding, }, }; } return { props: { ...(locale ? await serverSideTranslations(locale, ['common']) : {}), authFlow, SAMLResponse: null, connections: connectionsTransformed, branding, }, }; };