Merge pull request #804 from Infisical/jumpcloud-saml

Optimize SAML SSO configuration flow and add documentation for Azure AD SAML
This commit is contained in:
BlackMagiq
2023-07-29 15:01:41 +07:00
committed by GitHub
44 changed files with 347 additions and 260 deletions
+2 -1
View File
@@ -4,7 +4,8 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
import {
getAuthToken,
getMfaTempToken,
getSignupTempToken} from "@app/reactQuery";
getSignupTempToken
} from "@app/reactQuery";
export const apiRequest = axios.create({
baseURL: "/",
+4 -10
View File
@@ -29,8 +29,7 @@ export const useCreateSSOConfig = () => {
isActive,
entryPoint,
issuer,
cert,
audience
cert
}: {
organizationId: string;
authProvider: string;
@@ -38,7 +37,6 @@ export const useCreateSSOConfig = () => {
entryPoint: string;
issuer: string;
cert: string;
audience: string;
}) => {
const { data } = await apiRequest.post(
"/api/v1/sso/config",
@@ -48,8 +46,7 @@ export const useCreateSSOConfig = () => {
isActive,
entryPoint,
issuer,
cert,
audience
cert
}
);
@@ -70,8 +67,7 @@ export const useUpdateSSOConfig = () => {
isActive,
entryPoint,
issuer,
cert,
audience
cert
}: {
organizationId: string;
authProvider?: string;
@@ -79,7 +75,6 @@ export const useUpdateSSOConfig = () => {
entryPoint?: string;
issuer?: string;
cert?: string;
audience?: string;
}) => {
const { data } = await apiRequest.patch(
"/api/v1/sso/config",
@@ -89,8 +84,7 @@ export const useUpdateSSOConfig = () => {
...(isActive !== undefined ? { isActive } : {}),
...(entryPoint !== undefined ? { entryPoint } : {}),
...(issuer !== undefined ? { issuer } : {}),
...(cert !== undefined ? { cert } : {}),
...(audience !== undefined ? { audience } : {})
...(cert !== undefined ? { cert } : {})
}
);
@@ -5,6 +5,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useCreateSSOConfig,
useGetSSOConfig,
useUpdateSSOConfig
} from "@app/hooks/api";
@@ -13,7 +14,8 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { SSOModal } from "./SSOModal";
const ssoAuthProviderMap: { [key: string]: string } = {
"okta-saml": "Okta SAML 2.0"
"okta-saml": "Okta SAML",
"azure-saml": "Azure SAML"
}
export const OrgSSOSection = (): JSX.Element => {
@@ -27,6 +29,8 @@ export const OrgSSOSection = (): JSX.Element => {
"addSSO"
] as const);
const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
const handleSamlSSOToggle = async (value: boolean) => {
try {
if (!currentOrg?._id) return;
@@ -49,6 +53,31 @@ export const OrgSSOSection = (): JSX.Element => {
}
}
const addSSOBtnClick = async () => {
try {
if (subscription?.samlSSO && currentOrg) {
if (!data) {
// case: SAML SSO is not configured
// -> initialize empty SAML SSO configuration
await createMutateAsync({
organizationId: currentOrg._id,
authProvider: "okta-saml",
isActive: false,
entryPoint: "",
issuer: "",
cert: ""
});
}
handlePopUpOpen("addSSO");
} else {
handlePopUpOpen("upgradePlan");
}
} catch (err) {
console.error(err);
}
}
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
@@ -57,13 +86,7 @@ export const OrgSSOSection = (): JSX.Element => {
</h2>
{!isLoading && (
<Button
onClick={() => {
if (subscription?.samlSSO) {
handlePopUpOpen("addSSO");
} else {
handlePopUpOpen("upgradePlan");
}
}}
onClick={addSSOBtnClick}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
@@ -71,39 +94,33 @@ export const OrgSSOSection = (): JSX.Element => {
</Button>
)}
</div>
{!isLoading && data && (
<>
<div className="mb-4">
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data.isActive}
>
Enable SAML SSO
</Switch>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
<p className="text-gray-400 text-md">{data._id}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3>
<p className="text-gray-400 text-md">{ssoAuthProviderMap[data.authProvider]}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Audience</h3>
<p className="text-gray-400 text-md">{data.audience}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
<p className="text-gray-400 text-md">{data.entryPoint}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
<p className="text-gray-400 text-md">{data.issuer}</p>
</div>
</>
{data && (
<div className="mb-4">
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data ? data.isActive : false}
>
Enable SAML SSO
</Switch>
</div>
)}
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
<p className="text-gray-400 text-md">{(data && data._id !== "") ? data._id : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3>
<p className="text-gray-400 text-md">{(data && data.authProvider !== "") ? ssoAuthProviderMap[data.authProvider] : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
<p className="text-gray-400 text-md">{(data && data.entryPoint !== "") ? data.entryPoint : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
<p className="text-gray-400 text-md">{(data && data.issuer !== "") ? data.issuer : "-"}</p>
</div>
<SSOModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -21,16 +21,21 @@ import {
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum AuthProvider {
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml"
}
const ssoAuthProviders = [
{ label: "Okta SAML 2.0", value: "okta-saml" }
{ label: "Okta SAML", value: AuthProvider.OKTA_SAML },
{ label: "Azure SAML", value: AuthProvider.AZURE_SAML }
];
const schema = yup.object({
authProvider: yup.string().required("SSO Type is required"),
entryPoint: yup.string().required("IDP entrypoint is required"),
entryPoint: yup.string().required("IdP entrypoint is required"),
issuer: yup.string().required("Issuer string is required"),
cert: yup.string().required("IDP's public signing certificate is required"),
audience: yup.string().required("Expected SAML response audience is required"),
cert: yup.string().required("IdP's public signing certificate is required")
}).required();
export type AddSSOFormData = yup.InferType<typeof schema>;
@@ -59,7 +64,7 @@ export const SSOModal = ({
watch,
} = useForm<AddSSOFormData>({
defaultValues: {
authProvider: "okta-saml"
authProvider: AuthProvider.OKTA_SAML
},
resolver: yupResolver(schema)
});
@@ -70,8 +75,7 @@ export const SSOModal = ({
authProvider: data?.authProvider ?? "",
entryPoint: data?.entryPoint ?? "",
issuer: data?.issuer ?? "",
cert: data?.cert ?? "",
audience: data?.audience ?? ""
cert: data?.cert ?? ""
});
}
}, [data]);
@@ -80,8 +84,7 @@ export const SSOModal = ({
authProvider,
entryPoint,
issuer,
cert,
audience
cert
}: AddSSOFormData) => {
try {
if (!currentOrg) return;
@@ -93,8 +96,7 @@ export const SSOModal = ({
isActive: false,
entryPoint,
issuer,
cert,
audience
cert
});
} else {
await updateMutateAsync({
@@ -103,8 +105,7 @@ export const SSOModal = ({
isActive: false,
entryPoint,
issuer,
cert,
audience
cert
});
}
@@ -123,6 +124,38 @@ export const SSOModal = ({
}
}
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 AD Identifier",
issuerPlaceholder: "https://sts.windows.net/xxx/"
});
default:
return ({
acsUrl: "ACS URL",
entityId: "Entity ID",
entryPoint: "Entrypoint",
entryPointPlaceholder: "Enter entrypoint...",
issuer: "Issuer",
issuerPlaceholder: "Enter placeholder..."
});
}
}
const authProvider = watch("authProvider");
return (
@@ -160,36 +193,28 @@ export const SSOModal = ({
</FormControl>
)}
/>
{authProvider && authProvider === "okta-saml" && (
{authProvider && data && (
<>
<Controller
control={control}
name="audience"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Audience"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://your-domain.com"
/>
</FormControl>
)}
/>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">{renderLabels(authProvider).acsUrl}</h3>
<p className="text-gray-400 text-md break-all">{`${window.origin}/api/v1/sso/saml2/${data._id}`}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">{renderLabels(authProvider).entityId}</h3>
<p className="text-gray-400 text-md">{window.origin}</p>
</div>
<Controller
control={control}
name="entryPoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Entrypoint"
label={renderLabels(authProvider).entryPoint}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://your-domain.okta.com/app/app-name/xxx/sso/saml"
placeholder={renderLabels(authProvider).entryPointPlaceholder}
/>
</FormControl>
)}
@@ -199,13 +224,13 @@ export const SSOModal = ({
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Issuer"
label={renderLabels(authProvider).issuer}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="http://www.okta.com/xxx"
placeholder={renderLabels(authProvider).issuerPlaceholder}
/>
</FormControl>
)}