import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal, FormControl, Input, Modal, ModalContent, Select, SelectItem, TextArea } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useToggle } from "@app/hooks"; import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { SSOModalHeader } from "./SSOModalHeader"; enum AuthProvider { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", KEYCLOAK_SAML = "keycloak-saml", GOOGLE_SAML = "google-saml", AUTH0_SAML = "auth0-saml" } const ssoAuthProviders = [ { label: "Okta SAML", value: AuthProvider.OKTA_SAML, image: "Okta.png", docsUrl: "okta" }, { label: "Azure / Entra SAML", value: AuthProvider.AZURE_SAML, image: "Microsoft Azure.png", docsUrl: "azure" }, { label: "JumpCloud SAML", value: AuthProvider.JUMPCLOUD_SAML, image: "JumpCloud.png", docsUrl: "jumpcloud" }, { label: "Keycloak SAML", value: AuthProvider.KEYCLOAK_SAML, image: "Keycloak.png", docsUrl: "keycloak-saml" }, { label: "Google SAML", value: AuthProvider.GOOGLE_SAML, image: "Google.png", docsUrl: "google-saml" }, { label: "Auth0 SAML", value: AuthProvider.AUTH0_SAML, image: "Auth0.png", docsUrl: "auth0-saml" } ]; const schema = z .object({ authProvider: z.string().min(1, "SSO Type is required"), entryPoint: z.string().default(""), issuer: z.string().default(""), cert: z.string().default("") }) .required(); export type AddSSOFormData = z.infer; type Props = { popUp: UsePopUpState<["addSSO"]>; handlePopUpClose: (popUpName: keyof UsePopUpState<["addSSO"]>) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addSSO"]>, state?: boolean) => void; hideDelete?: boolean; }; export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelete }: Props) => { const { currentOrg } = useOrganization(); const { mutateAsync: createMutateAsync, isPending: createIsLoading } = useCreateSSOConfig(); const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateSSOConfig(); const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(); const { data } = useGetSSOConfig(currentOrg?.id ?? ""); const { control, handleSubmit, reset, watch, setValue, getValues } = useForm({ defaultValues: { authProvider: AuthProvider.OKTA_SAML }, resolver: zodResolver(schema) }); useEffect(() => { if (data) { reset({ authProvider: data?.authProvider ?? "", entryPoint: data?.entryPoint ?? "", issuer: data?.issuer ?? "", cert: data?.cert ?? "" }); } }, [data]); const handleSamlSoftDelete = async () => { if (!currentOrg) { return; } try { await updateMutateAsync({ organizationId: currentOrg.id, isActive: false, entryPoint: "", issuer: "", cert: "" }); createNotification({ text: "Successfully deleted SAML SSO configuration.", type: "success" }); } catch { createNotification({ text: "Failed deleting SAML SSO configuration.", type: "error" }); } }; const onSSOModalSubmit = async ({ authProvider, entryPoint, issuer, cert }: AddSSOFormData) => { try { if (!currentOrg) return; if (!data) { await createMutateAsync({ organizationId: currentOrg.id, authProvider, isActive: false, entryPoint, issuer, cert }); } else { await updateMutateAsync({ organizationId: currentOrg.id, authProvider, isActive: false, entryPoint, issuer, cert }); } handlePopUpClose("addSSO"); createNotification({ text: `Successfully ${!data ? "added" : "updated"} SAML SSO configuration`, type: "success" }); } catch (err) { console.error(err); createNotification({ text: `Failed to ${!data ? "add" : "update"} SAML SSO configuration`, type: "error" }); } }; const renderLabels = (authProvider: string) => { switch (authProvider) { case AuthProvider.OKTA_SAML: return { acsUrl: "Single sign-on URL", entityId: "Audience URI (SP Entity ID)", entryPoint: "Identity Provider Single Sign-On URL", entryPointPlaceholder: "https://your-domain.okta.com/app/app-name/xxx/sso/saml", issuer: "Identity Provider Issuer", issuerPlaceholder: "http://www.okta.com/xxx" }; case AuthProvider.AZURE_SAML: return { acsUrl: "Reply URL (Assertion Consumer Service URL)", entityId: "Identifier (Entity ID)", entryPoint: "Login URL", entryPointPlaceholder: "https://login.microsoftonline.com/xxx/saml2", issuer: "Azure / Entra Application ID", issuerPlaceholder: "abc-def-ghi-jkl-mno" }; case AuthProvider.JUMPCLOUD_SAML: return { acsUrl: "ACS URL", entityId: "SP Entity ID", entryPoint: "IDP URL", entryPointPlaceholder: "https://sso.jumpcloud.com/saml2/xxx", issuer: "IdP Entity ID", issuerPlaceholder: "xxx" }; case AuthProvider.KEYCLOAK_SAML: return { acsUrl: "Valid redirect URI", entityId: "SP Entity ID", entryPoint: "IDP URL", entryPointPlaceholder: "https://keycloak.mysite.com/realms/myrealm/protocol/saml", issuer: "Client ID", issuerPlaceholder: window.origin }; case AuthProvider.GOOGLE_SAML: return { acsUrl: "ACS URL", entityId: "SP Entity ID", entryPoint: "SSO URL", entryPointPlaceholder: "https://accounts.google.com/o/saml2/idp?idpid=xxx", issuer: "Issuer", issuerPlaceholder: window.origin }; case AuthProvider.AUTH0_SAML: return { acsUrl: "Application Callback URL", entityId: "Audience", entryPoint: "Identity Provider Login URL", entryPointPlaceholder: "https://xxx.auth0.com/samlp/xxx", issuer: "Issuer", issuerPlaceholder: "urn:xxx-xxx.us.auth0.com" }; default: return { acsUrl: "ACS URL", entityId: "Entity ID", entryPoint: "Entrypoint", entryPointPlaceholder: "Enter entrypoint...", issuer: "Issuer", issuerPlaceholder: "Enter placeholder..." }; } }; const authProvider = watch("authProvider"); useEffect(() => { if (authProvider === AuthProvider.GOOGLE_SAML && getValues("issuer") === "") { setValue("issuer", window.origin); } }, [authProvider]); return ( <> { handlePopUpToggle("addSSO", isOpen); reset(); }} > provider.value === authProvider)!} isConnected={Boolean(data)} />
( )} /> {authProvider && data && ( <>

{renderLabels(authProvider).acsUrl}

{`${window.origin}/api/v1/sso/saml2/${data.id}`}

{renderLabels(authProvider).entityId}

{window.origin}

( )} /> ( )} /> (