diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/2.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/2.tsx new file mode 100644 index 0000000..b559737 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/2.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import SalesforceOidcStep0Image from "@app/images/salesforce/OIDC/salesforce_oidc_0.png"; +import SalesforceOidcStep1Image from "@app/images/salesforce/OIDC/salesforce_oidc_1.png"; +import { + InstructionProps, + Step, + ClipboardCopyComponent, + StepImage, +} from "@wizardComponents"; + +type Props = { + loginRedirectURL: string; +}; + +export const SalesforceStepTwo: React.FC = ({ loginRedirectURL }) => { + const instructions: InstructionProps[] = [ + { + text: ( +
+ Under the API (Enable OAuth Settings) section, check the Enable OAuth Settings{" "} + checkbox and paste the URL from below into the Callback URL field. +
+ ), + component: , + }, + { + component: ( + + ), + }, + { + text: ( +
+ Select the Access the identity URL service (id, profile, email, address, phone) and{" "} + Access unique user identifiers (openid) OAuth Scopes, then click the Add button.{" "} +

+ Uncheck the Require Proof Key for Code Exchange (PKCE) Extension for Supported Authorization Flows checkbox. +
+ ), + component: , + }, + { + component: ( +
+ Click the Save button at the bottom of the page to save your changes. +
+ ), + } + ]; + + return ( + + ); +}; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/3.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/3.tsx new file mode 100644 index 0000000..4fbdf88 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/3.tsx @@ -0,0 +1,74 @@ +import { Card, CardBody } from "@patternfly/react-core"; +import React, { FC } from "react"; +import SalesforceOidcStep2Image from "@app/images/salesforce/oidc/salesforce_oidc_2.png"; +import SalesforceOidcStep3Image from "@app/images/salesforce/oidc/salesforce_oidc_3.png"; +import SalesforceOidcStep4Image from "@app/images/salesforce/oidc/salesforce_oidc_4.png"; +import { InstructionProps, Step, StepImage } from "@wizardComponents"; +import { API_RETURN_PROMISE } from "@app/configurations/api-status"; +import { ClientCredentials } from "../forms"; + +interface Props { + onFormSubmission: ({ + domain, + clientId, + clientSecret, + }: { + domain: string; + clientId: string; + clientSecret: string; + }) => API_RETURN_PROMISE; + values: { + domain: string; + clientId: string; + clientSecret: string; + }; +} + +export const SalesforceStepThree: FC = ({ onFormSubmission, values }) => { + const instructions: InstructionProps[] = [ + { + text: ( +
+ On the next page, press the Manage Consumer Details button under the API (Enable OAuth Settings) section to view your app's credentials. +
+ ), + component: , + }, + { + text: ( +
+ Copy the Consumer Key and Consumer Secret and paste them into the fields below. +
+ ), + component: , + }, + { + text: ( +
+ In the sidebar menu, click My Domain under Company Settings to view your domain.{" "} + Copy the domain listed under Current My Domain URL and paste it into the domain field below. +
+ ), + component: , + }, + { + component: ( + + + + + + ), + }, + ]; + + return ( + + ); +}; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/index.ts b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/index.ts new file mode 100644 index 0000000..ceac374 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/Steps/index.ts @@ -0,0 +1,2 @@ +export * from "./2"; +export * from "./3"; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/forms/client-credentials.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/forms/client-credentials.tsx new file mode 100644 index 0000000..6c0e64f --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/forms/client-credentials.tsx @@ -0,0 +1,152 @@ +import React, { FC, useState } from "react"; +import { useFormik } from "formik"; +import { + ActionGroup, + Alert, + Button, + Form, + FormAlert, + FormGroup, + TextInput, +} from "@patternfly/react-core"; +import * as Yup from "yup"; +import { + API_RETURN, + API_RETURN_PROMISE, + API_STATUS, +} from "@app/configurations/api-status"; + +export type ClientCreds = { + domain: string; + clientId: string; + clientSecret: string; +}; + +const ClientCredentialsSchema = Yup.object().shape({ + clientId: Yup.string().required("Consumer Key is a required field."), + clientSecret: Yup.string().required("Consumer Secret is a required field."), + domain: Yup.string().required("Domain is a required field."), +}); + +type Props = { + handleFormSubmit: ({ + domain, + clientId, + clientSecret, + }: ClientCreds) => API_RETURN_PROMISE; + formActive?: boolean; + credentials: ClientCreds; +}; + +export const ClientCredentials: FC = ({ + handleFormSubmit, + formActive = true, + credentials, +}) => { + const [submissionResp, setSubmissionResp] = useState(); + const { + handleSubmit, + handleChange, + values, + errors, + touched, + isSubmitting, + setSubmitting, + } = useFormik({ + initialValues: { + domain: credentials.domain, + clientId: credentials.clientId, + clientSecret: credentials.clientSecret, + }, + onSubmit: async (values) => { + const resp = await handleFormSubmit(values); + setSubmissionResp(resp); + setSubmitting(false); + }, + validationSchema: ClientCredentialsSchema, + }); + + const hasError = (key: string) => + errors[key] && touched[key] ? "error" : "default"; + + return ( +
+ {submissionResp && ( + + + + )} + + handleChange(e)} + validated={hasError("clientId")} + isDisabled={!formActive} + /> + + + handleChange(e)} + validated={hasError("clientSecret")} + isDisabled={!formActive} + type="password" + /> + + + handleChange(e)} + validated={hasError("domain")} + isDisabled={!formActive} + placeholder="mydomain.my.salesforce.com" + /> + + + + +
+ ); +}; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/forms/index.ts b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/forms/index.ts new file mode 100644 index 0000000..a405c63 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/forms/index.ts @@ -0,0 +1 @@ +export * from "./client-credentials"; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/index.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/index.tsx new file mode 100644 index 0000000..6f27cdc --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/OIDC/index.tsx @@ -0,0 +1,234 @@ +import React, { FC, useEffect, useState } from "react"; +import { + PageSection, + PageSectionVariants, + PageSectionTypes, + Wizard, +} from "@patternfly/react-core"; +import * as Steps from "./Steps"; +import * as SharedSteps from "../shared/Steps"; +import salesforceLogo from "@app/images/salesforce/salesforce-logo.png"; +import { WizardConfirmation, Header } from "@wizardComponents"; +import { useKeycloakAdminApi } from "@app/hooks/useKeycloakAdminApi"; +import { API_STATUS } from "@app/configurations/api-status"; +import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; +import { useNavigateToBasePath } from "@app/routes"; +import { OidcDefaults, Protocols, Providers } from "@app/configurations"; +import { Axios, clearAlias, getAlias, CreateIdp } from "@wizardServices"; +import { useApi, usePrompt } from "@app/hooks"; +import { useGetFeatureFlagsQuery } from "@app/services"; + +export const SalesforceWizardOIDC: FC = () => { + const idpCommonName = "Salesforce OIDC IdP"; + + const { data: featureFlags } = useGetFeatureFlagsQuery(); + const navigateToBasePath = useNavigateToBasePath(); + const { getRealm } = useKeycloakAdminApi(); + const { + alias, + setAlias, + loginRedirectURL, + identifierURL, + createIdPUrl, + adminLinkOidc: adminLink, + } = useApi(); + + const [stepIdReached, setStepIdReached] = useState(1); + const [results, setResults] = useState(""); + const [error, setError] = useState(false); + const [disableButton, setDisableButton] = useState(false); + + const [isFormValid, setIsFormValid] = useState(false); + const [isValidating, setIsValidating] = useState(false); + const [validationResults, setValidationResults] = useState< + Record | undefined + >({}); + const [config, setConfig] = useState({ + domain: "", + clientId: "", + clientSecret: "", + }); + + useEffect(() => { + const genAlias = getAlias({ + provider: Providers.SALESFORCE, + protocol: Protocols.OPEN_ID, + preface: "salesforce-oidc", + }); + setAlias(genAlias); + }, []); + + const finishStep = 5; + + usePrompt( + "The wizard is incomplete. Leaving will lose any saved progress. Are you sure?", + stepIdReached < finishStep + ); + + const onNext = (newStep) => { + if (stepIdReached === finishStep) { + clearAlias({ + provider: Providers.SALESFORCE, + protocol: Protocols.OPEN_ID, + }); + navigateToBasePath(); + } + setStepIdReached(stepIdReached < newStep.id ? newStep.id : stepIdReached); + }; + + const closeWizard = () => navigateToBasePath(); + + const handleFormSubmission = async ({ + domain, + clientId, + clientSecret, + }: { + domain: string; + clientId: string; + clientSecret: string; + }) => { + const trustedDomain = `https://${domain}/.well-known/openid-configuration`; + setConfig({ domain, clientId, clientSecret }); + + let resp; + try { + const payload = { + fromUrl: trustedDomain, + providerId: "oidc", + realm: getRealm(), + }; + + resp = await Axios.post(identifierURL, payload); + + if (resp.status === 200) { + setIsFormValid(true); + setValidationResults(resp.data); + return { + status: API_STATUS.SUCCESS, + message: "Domain and credentials validated. Please continue.", + }; + } + } catch (e) { + console.log(e); + } + setIsFormValid(false); + return { + status: API_STATUS.ERROR, + message: "Domain and credentials invalid, please check and try again.", + }; + }; + + const createIdP = async () => { + setIsValidating(true); + setResults(`Creating ${idpCommonName}...`); + + const payload: IdentityProviderRepresentation = { + alias, + displayName: `Salesforce OIDC Single Sign-on`, + providerId: "oidc", + config: { + ...OidcDefaults, + ...validationResults, + clientId: config.clientId, + clientSecret: config.clientSecret, + }, + }; + + try { + await CreateIdp({ createIdPUrl, payload, featureFlags }); + // TODO emailAsUsername, Mapper? + + setResults(`${idpCommonName} created successfully. Click finish.`); + setStepIdReached(finishStep); + setError(false); + setDisableButton(true); + clearAlias({ + provider: Providers.SALESFORCE, + protocol: Protocols.OPEN_ID, + }); + } catch (e) { + setResults(`Error creating ${idpCommonName}.`); + setError(true); + } finally { + setIsValidating(false); + } + + setIsValidating(false); + }; + + const steps = [ + { + id: 1, + name: "Create Connected App", + component: , + hideCancelButton: true, + canJumpTo: stepIdReached >= 1, + }, + { + id: 2, + name: "Configure OAuth Settings", + component: , + hideCancelButton: true, + canJumpTo: stepIdReached >= 2, + }, + { + id: 3, + name: "Provide Credentials and Domain", + component: ( + + ), + hideCancelButton: true, + canJumpTo: stepIdReached >= 3, + enableNext: isFormValid, + }, + { + id: 4, + name: "Confirmation", + component: ( + + ), + nextButtonText: "Finish", + hideCancelButton: true, + enableNext: stepIdReached === finishStep, + canJumpTo: stepIdReached >= 4, + }, + ]; + + const title = "Salesforce wizard"; + + return ( + <> +
+ + + + + ); +}; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/1.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/1.tsx new file mode 100644 index 0000000..9b450a3 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/1.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import SalesforceSamlStep0Image from "@app/images/salesforce/SAML/salesforce_saml_0.png"; +import { InstructionProps, Step, StepImage } from "@wizardComponents"; + +export function SalesforceStepOne() { + const instructions: InstructionProps[] = [ + { + text: ( +
+ In the Salesforce Console, open the Setup menu and select{" "} + Identity Provider under Identity. Verify that your{" "} + Salesforce identity provider has been enabled, or click Enable{" "} + Identity Provider to enable it. +
+ ), + component: , + }, + ]; + + return ( + + ); +} diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/3.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/3.tsx new file mode 100644 index 0000000..9cd57ab --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/3.tsx @@ -0,0 +1,50 @@ +import React, { FC } from "react"; +import SalesforceSamlStep1Image from "@app/images/salesforce/SAML/salesforce_saml_1.png"; +import { InstructionProps, Step, StepImage, ClipboardCopyComponent } from "@wizardComponents"; + +type Props = { + acsUrl: string; + entityId: string; +}; + +export const SalesforceStepThree: FC = ({ acsUrl, entityId }) => { + const instructions: InstructionProps[] = [ + { + text: ( +
+ Under Web App Settings, check the Enable SAML checkbox, then{" "} + paste the Entity Id and ACS URL into the appropriate fields. +
+ ), + component: , + }, + { + component: ( + <> + + + + ), + }, + { + component: ( +
+ Click the Save button at the bottom of the page to save your changes. +
+ ), + } + ]; + + return ( + + ); +} diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/4.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/4.tsx new file mode 100644 index 0000000..504ba16 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/4.tsx @@ -0,0 +1,52 @@ +import React, { FC } from "react"; +import SalesforceSamlStep2Image from "@app/images/salesforce/SAML/salesforce_saml_2.png"; +import SalesforceCommonStep3Image from "@app/images/salesforce/COMMON/salesforce-3.png"; +import { + FileCard, + InstructionProps, + Step, + StepImage, + UrlForm, +} from "@wizardComponents"; +import { API_RETURN_PROMISE } from "@app/configurations/api-status"; + +interface Props { + url: string; + handleFormSubmit: ({ url }: { url: string }) => API_RETURN_PROMISE; +} + +export const SalesforceStepFour: FC = ({ url, handleFormSubmit }) => { + const instructions: InstructionProps[] = [ + { + text: ( +
+ On the next page, click the Manage button to view your app's SAML settings. +
+ ), + component: , + }, + { + text: ( +
+ Under the SAML Login Information section, copy the Metadata Discovery Endpoint URL{" "} + and paste it into the field below. +
+ ), + component: , + }, + { + component: ( + + + + ), + }, + ]; + + return ( + + ); +}; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/5.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/5.tsx new file mode 100644 index 0000000..65cff74 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/5.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import SalesforceCommonStep4Image from "@app/images/salesforce/COMMON/salesforce-4.png"; +import SalesforceCommonStep5Image from "@app/images/salesforce/COMMON/salesforce-5.png"; +import { InstructionProps, Step, StepImage } from "@wizardComponents"; + +export function SalesforceStepFive() { + const instructions: InstructionProps[] = [ + { + text: ( +
+ Under the Profiles section, click Manage Profiles to assign the connected app to the appropriate profiles. +
+ ), + component: , + }, + { + text: ( +
+ Select the desired profiles then click Save at the bottom of the page. +
+ ), + component: , + } + ]; + + return ( + + ); +} diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/6.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/6.tsx new file mode 100644 index 0000000..4b7640e --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/6.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import SalesforceSamlStep3Image from "@app/images/salesforce/SAML/salesforce_saml_3.png"; +import SalesforceSamlStep4Image from "@app/images/salesforce/SAML/salesforce_saml_4.png"; +import { + InstructionProps, + Step, + DoubleItemClipboardCopy, + StepImage, +} from "@wizardComponents"; + +export function SalesforceStepSix() { + const instructions: InstructionProps[] = [ + { + text: ( +
+ Under the Custom Attributes section, provide the following attribute{" "} + mappings by selecting New for each attribute listed below. +
+ ), + component: , + }, + { + text: ( +
+ Copy the Attribute Key and Attribute Value for each attribute into the{" "} + respective fields. +
+ ), + component: , + }, + { + component: ( + <> + + + + ), + }, + ]; + + return ( + + ); +}; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/index.ts b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/index.ts new file mode 100644 index 0000000..6887ffb --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/Steps/index.ts @@ -0,0 +1,5 @@ +export * from "./1"; +export * from "./3"; +export * from "./4"; +export * from "./5"; +export * from "./6"; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/index.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/index.tsx new file mode 100644 index 0000000..cc3bcf9 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/SAML/index.tsx @@ -0,0 +1,257 @@ +import React, { FC, useState, useEffect } from "react"; +import { + PageSection, + PageSectionVariants, + PageSectionTypes, + Wizard, +} from "@patternfly/react-core"; +import { API_STATUS, METADATA_CONFIG, API_RETURN } from "@app/configurations/api-status"; +import { Axios, clearAlias } from "@wizardServices"; +import * as Steps from "./Steps"; +import * as SharedSteps from "../shared/Steps"; +import salesforceLogo from "@app/images/salesforce/salesforce-logo.png"; +import { WizardConfirmation, Header } from "@wizardComponents"; +import { useKeycloakAdminApi } from "@app/hooks/useKeycloakAdminApi"; +import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; +import { + CreateIdp, + SamlAttributeMapper, +} from "@app/components/IdentityProviderWizard/Wizards/services"; +import { useNavigateToBasePath } from "@app/routes"; +import { getAlias } from "@wizardServices"; +import { Protocols, Providers, SamlIDPDefaults } from "@app/configurations"; +import { useApi, usePrompt } from "@app/hooks"; +import { useGetFeatureFlagsQuery } from "@app/services"; + +export const SalesforceWizardSAML: FC = () => { + const idpCommonName = "Salesforce SAML IdP"; + + const navigateToBasePath = useNavigateToBasePath(); + const { data: featureFlags } = useGetFeatureFlagsQuery(); + const { + alias, + setAlias, + adminLinkSaml: adminLink, + identifierURL, + createIdPUrl, + loginRedirectURL: acsUrl, + entityId, + } = useApi(); + + useEffect(() => { + const genAlias = getAlias({ + provider: Providers.SALESFORCE, + protocol: Protocols.SAML, + preface: "salesforce-saml", + }); + setAlias(genAlias); + }, []); + + const [stepIdReached, setStepIdReached] = useState(1); + const [results, setResults] = useState(""); + const [error, setError] = useState(false); + const [disableButton, setDisableButton] = useState(false); + const [isFormValid, setIsFormValid] = useState(false); + + const [configData, setConfigData] = useState(null); + const [metadataUrl, setMetadataUrl] = useState(""); + const [isValidating, setIsValidating] = useState(false); + const { getRealm } = useKeycloakAdminApi(); + + const finishStep = 8; + + usePrompt( + "The wizard is incomplete. Leaving will lose any saved progress. Are you sure?", + stepIdReached < finishStep + ); + + const onNext = (newStep) => { + if (stepIdReached === finishStep) { + clearAlias({ + provider: Providers.SALESFORCE, + protocol: Protocols.SAML, + }); + navigateToBasePath(); + } + setStepIdReached(stepIdReached < newStep.id ? newStep.id : stepIdReached); + }; + + const closeWizard = () => navigateToBasePath(); + + const handleFormSubmit = async ({ + url, + }: { + url: string; + }): Promise => { + // make call to submit the URL and verify it + setMetadataUrl(metadataUrl); + + try { + const payload = { + fromUrl: url, + providerId: "saml", + realm: getRealm(), + }; + const resp = await Axios.post(identifierURL, payload); + + if (resp.status === 200) { + setConfigData({ + ...SamlIDPDefaults, + ...resp.data, + }); + setIsFormValid(true); + return { + status: API_STATUS.SUCCESS, + message: `Configuration successfully validated with ${idpCommonName}. Continue to next step.`, + }; + } + } catch (err) { + console.log(err); + } + + return { + status: API_STATUS.ERROR, + message: `Configuration validation failed with ${idpCommonName}. Check file and try again.`, + }; + }; + + const createIdP = async () => { + setIsValidating(true); + setResults(`Creating ${idpCommonName}...`); + + const payload: IdentityProviderRepresentation = { + alias, + displayName: `Salesforce SAML Single Sign-on`, + providerId: "saml", + config: configData!, + }; + + try { + await CreateIdp({ createIdPUrl, payload, featureFlags }); + + await SamlAttributeMapper({ + alias, + createIdPUrl, + usernameAttribute: { attributeName: "username", friendlyName: "" }, + emailAttribute: { attributeName: "email", friendlyName: "" }, + firstNameAttribute: { attributeName: "firstName", friendlyName: "" }, + lastNameAttribute: { attributeName: "lastName", friendlyName: "" }, + featureFlags, + }); + + setResults(`${idpCommonName} created successfully. Click finish.`); + setStepIdReached(finishStep); + setError(false); + setDisableButton(true); + clearAlias({ + provider: Providers.SALESFORCE, + protocol: Protocols.SAML, + }); + } catch (e) { + setResults(`Error creating ${idpCommonName}.`); + setError(true); + } finally { + setIsValidating(false); + } + + setIsValidating(false); + }; + + const steps = [ + { + id: 1, + name: "Enable Identity Provider", + component: , + hideCancelButton: true, + enabledNext: true, + canJumpTo: stepIdReached >= 1, + }, + { + id: 2, + name: "Create Connected App", + component: , + hideCancelButton: true, + enableNext: true, + canJumpTo: stepIdReached >= 2, + }, + { + id: 3, + name: "Enter Service Provider Details", + component: , + hideCancelButton: true, + enableNext: true, + canJumpTo: stepIdReached >= 3, + }, + { + id: 4, + name: "Upload Salesforce IdP Information", + component: ( + + ), + hideCancelButton: true, + canJumpTo: stepIdReached >= 4, + enableNext: isFormValid, + }, + { + id: 5, + name: "Assign Profiles", + component: , + hideCancelButton: true, + enabledNext: true, + canJumpTo: stepIdReached >= 5, + }, + { + id: 6, + name: "Configure Attribute Mapping", + component: , + hideCancelButton: true, + canJumpTo: stepIdReached >= 6, + }, + { + id: 7, + name: "Confirmation", + component: ( + + ), + nextButtonText: "Finish", + hideCancelButton: true, + enableNext: stepIdReached === finishStep, + canJumpTo: stepIdReached >= 7, + }, + ]; + + const title = "Salesforce wizard"; + + return ( + <> +
+ + + + + ); +}; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/index.ts b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/index.ts new file mode 100644 index 0000000..7a3cd29 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/index.ts @@ -0,0 +1,2 @@ +export * from "./OIDC"; +export * from "./SAML"; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/shared/Steps/ConnectedApp.tsx b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/shared/Steps/ConnectedApp.tsx new file mode 100644 index 0000000..5db7e07 --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/shared/Steps/ConnectedApp.tsx @@ -0,0 +1,47 @@ +import React, { FC } from "react"; +import SalesforceCommonStep0Image from "@app/images/salesforce/COMMON/salesforce-0.png"; +import SalesforceCommonStep1Image from "@app/images/salesforce/COMMON/salesforce-1.png"; +import SalesforceCommonStep2Image from "@app/images/salesforce/COMMON/salesforce-2.png"; +import { InstructionProps, Step, StepImage } from "@wizardComponents"; + +type Props = { + stepNumber: number; +}; + +export const SalesforceStepConnectedApp: FC = ({ stepNumber }) => { + const instructions: InstructionProps[] = [ + { + text: ( +
+ In the Salesforce Console, open the Setup menu and select{" "} + App Manager under Apps. Click New Connected App. +
+ ), + component: , + }, + { + text: ( +
+ Select Create a Connected App, then click Continue. +
+ ), + component: , + }, + { + text: ( +
+ Under the Basic Information section, enter a Connected App Name, API Name, and Contact Email. + The API Name will be automatically populated based on the Connected App Name. +
+ ), + component: , + }, + ]; + + return ( + + ); +} diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/shared/Steps/index.ts b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/shared/Steps/index.ts new file mode 100644 index 0000000..24c1fab --- /dev/null +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/Salesforce/shared/Steps/index.ts @@ -0,0 +1 @@ +export * from "./ConnectedApp"; diff --git a/src/app/components/IdentityProviderWizard/Wizards/Providers/index.ts b/src/app/components/IdentityProviderWizard/Wizards/Providers/index.ts index 1695f45..ece76e5 100644 --- a/src/app/components/IdentityProviderWizard/Wizards/Providers/index.ts +++ b/src/app/components/IdentityProviderWizard/Wizards/Providers/index.ts @@ -12,3 +12,4 @@ export * from "./Okta"; export * from "./OneLogin"; export * from "./Oracle"; export * from "./PingOne"; +export * from "./Salesforce"; diff --git a/src/app/components/IdentityProviderWizard/providers.tsx b/src/app/components/IdentityProviderWizard/providers.tsx index ebed971..92731b0 100644 --- a/src/app/components/IdentityProviderWizard/providers.tsx +++ b/src/app/components/IdentityProviderWizard/providers.tsx @@ -28,6 +28,8 @@ import { OneLoginWizard, OracleWizard, PingOneWizard, + SalesforceWizardOIDC, + SalesforceWizardSAML, } from "./Wizards"; import { useRoleAccess } from "@app/hooks"; import { Navigate, generatePath } from "react-router-dom"; @@ -70,6 +72,9 @@ const Provider = () => { return ; case Providers.DUO: return ; + case Providers.SALESFORCE: + if (protocol === Protocols.SAML) return ; + if (protocol === Protocols.OPEN_ID) return ; case Providers.LAST_PASS: return ; case Providers.CLOUDFLARE: diff --git a/src/app/configurations/identity-providers.ts b/src/app/configurations/identity-providers.ts index 1ffd7cb..9d0fd11 100644 --- a/src/app/configurations/identity-providers.ts +++ b/src/app/configurations/identity-providers.ts @@ -14,6 +14,7 @@ import samlLogo from "@app/images/provider-logos/saml_logo.svg"; import vmwareLogo from "@app/images/provider-logos/vmware_logo.svg"; import ldapLogo from "@app/images/provider-logos/ldap_logo.svg"; import duoLogo from "@app/images/duo/duo.svg"; +import salesforceLogo from "@app/images/salesforce/salesforce-logo.png"; import lastpassLogo from "@app/images/lastpass/lastpass-logo.svg"; import cloudflareLogo from "@app/images/cloudflare/cloudflare.svg"; import oracleLogo from "@app/images/oracle/oracle-logo.png"; @@ -37,6 +38,7 @@ export enum Providers { ORACLE = "oracle", PING_FEDERATE = "pingfederate", PING_ONE = "pingone", + SALESFORCE = "salesforce", SAML = "saml", VMWARE = "vmware", } @@ -148,6 +150,13 @@ export const IdentityProviders: IIDPType[] = [ protocols: [Protocols.SAML], }, { + name: "Salesforce", + imageSrc: salesforceLogo, + active: true, + id: Providers.SALESFORCE, + protocols: [Protocols.SAML, Protocols.OPEN_ID], + }, + { name: "LastPass", imageSrc: lastpassLogo, active: true, diff --git a/src/app/images/salesforce/COMMON/salesforce-0.png b/src/app/images/salesforce/COMMON/salesforce-0.png new file mode 100644 index 0000000..ebb4e82 Binary files /dev/null and b/src/app/images/salesforce/COMMON/salesforce-0.png differ diff --git a/src/app/images/salesforce/COMMON/salesforce-1.png b/src/app/images/salesforce/COMMON/salesforce-1.png new file mode 100644 index 0000000..6b6534a Binary files /dev/null and b/src/app/images/salesforce/COMMON/salesforce-1.png differ diff --git a/src/app/images/salesforce/COMMON/salesforce-2.png b/src/app/images/salesforce/COMMON/salesforce-2.png new file mode 100644 index 0000000..3394f7c Binary files /dev/null and b/src/app/images/salesforce/COMMON/salesforce-2.png differ diff --git a/src/app/images/salesforce/COMMON/salesforce-3.png b/src/app/images/salesforce/COMMON/salesforce-3.png new file mode 100644 index 0000000..b72eca4 Binary files /dev/null and b/src/app/images/salesforce/COMMON/salesforce-3.png differ diff --git a/src/app/images/salesforce/COMMON/salesforce-4.png b/src/app/images/salesforce/COMMON/salesforce-4.png new file mode 100644 index 0000000..b070ac4 Binary files /dev/null and b/src/app/images/salesforce/COMMON/salesforce-4.png differ diff --git a/src/app/images/salesforce/COMMON/salesforce-5.png b/src/app/images/salesforce/COMMON/salesforce-5.png new file mode 100644 index 0000000..1752ad3 Binary files /dev/null and b/src/app/images/salesforce/COMMON/salesforce-5.png differ diff --git a/src/app/images/salesforce/OIDC/salesforce_oidc_0.png b/src/app/images/salesforce/OIDC/salesforce_oidc_0.png new file mode 100644 index 0000000..4202323 Binary files /dev/null and b/src/app/images/salesforce/OIDC/salesforce_oidc_0.png differ diff --git a/src/app/images/salesforce/OIDC/salesforce_oidc_1.png b/src/app/images/salesforce/OIDC/salesforce_oidc_1.png new file mode 100644 index 0000000..36aacd8 Binary files /dev/null and b/src/app/images/salesforce/OIDC/salesforce_oidc_1.png differ diff --git a/src/app/images/salesforce/OIDC/salesforce_oidc_2.png b/src/app/images/salesforce/OIDC/salesforce_oidc_2.png new file mode 100644 index 0000000..2586a48 Binary files /dev/null and b/src/app/images/salesforce/OIDC/salesforce_oidc_2.png differ diff --git a/src/app/images/salesforce/OIDC/salesforce_oidc_3.png b/src/app/images/salesforce/OIDC/salesforce_oidc_3.png new file mode 100644 index 0000000..d8a115c Binary files /dev/null and b/src/app/images/salesforce/OIDC/salesforce_oidc_3.png differ diff --git a/src/app/images/salesforce/OIDC/salesforce_oidc_4.png b/src/app/images/salesforce/OIDC/salesforce_oidc_4.png new file mode 100644 index 0000000..b0e28b2 Binary files /dev/null and b/src/app/images/salesforce/OIDC/salesforce_oidc_4.png differ diff --git a/src/app/images/salesforce/SAML/salesforce_saml_0.png b/src/app/images/salesforce/SAML/salesforce_saml_0.png new file mode 100644 index 0000000..c22f2f2 Binary files /dev/null and b/src/app/images/salesforce/SAML/salesforce_saml_0.png differ diff --git a/src/app/images/salesforce/SAML/salesforce_saml_1.png b/src/app/images/salesforce/SAML/salesforce_saml_1.png new file mode 100644 index 0000000..f5c98f5 Binary files /dev/null and b/src/app/images/salesforce/SAML/salesforce_saml_1.png differ diff --git a/src/app/images/salesforce/SAML/salesforce_saml_2.png b/src/app/images/salesforce/SAML/salesforce_saml_2.png new file mode 100644 index 0000000..22e8cc6 Binary files /dev/null and b/src/app/images/salesforce/SAML/salesforce_saml_2.png differ diff --git a/src/app/images/salesforce/SAML/salesforce_saml_3.png b/src/app/images/salesforce/SAML/salesforce_saml_3.png new file mode 100644 index 0000000..85b3f61 Binary files /dev/null and b/src/app/images/salesforce/SAML/salesforce_saml_3.png differ diff --git a/src/app/images/salesforce/SAML/salesforce_saml_4.png b/src/app/images/salesforce/SAML/salesforce_saml_4.png new file mode 100644 index 0000000..fa1e731 Binary files /dev/null and b/src/app/images/salesforce/SAML/salesforce_saml_4.png differ diff --git a/src/app/images/salesforce/salesforce-logo.png b/src/app/images/salesforce/salesforce-logo.png new file mode 100644 index 0000000..6b0c1b1 Binary files /dev/null and b/src/app/images/salesforce/salesforce-logo.png differ