Merge pull request #1989 from Infisical/feature/oidc

feat: oidc
This commit is contained in:
Sheen Capadngan
2024-06-25 00:19:15 +08:00
committed by GitHub
78 changed files with 2355 additions and 68 deletions
+1
View File
@@ -5,6 +5,7 @@ export type TServerConfig = {
isMigrationModeOn?: boolean;
trustSamlEmails: boolean;
trustLdapEmails: boolean;
trustOidcEmails: boolean;
isSecretScanningDisabled: boolean;
};
+1
View File
@@ -17,6 +17,7 @@ export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys";
export * from "./ldapConfig";
export * from "./oidcConfig";
export * from "./organization";
export * from "./projectUserAdditionalPrivilege";
export * from "./rateLimit";
@@ -0,0 +1 @@
export * from "./queries";
@@ -0,0 +1,111 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { oidcConfigKeys } from "./queries";
export const useUpdateOIDCConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
issuer,
authorizationEndpoint,
configurationType,
discoveryURL,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
allowedEmailDomains,
clientId,
clientSecret,
isActive,
orgSlug
}: {
allowedEmailDomains?: string;
issuer?: string;
authorizationEndpoint?: string;
discoveryURL?: string;
jwksUri?: string;
tokenEndpoint?: string;
userinfoEndpoint?: string;
clientId?: string;
clientSecret?: string;
isActive?: boolean;
configurationType?: string;
orgSlug: string;
}) => {
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
issuer,
allowedEmailDomains,
authorizationEndpoint,
discoveryURL,
configurationType,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
orgSlug,
clientSecret,
isActive
});
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug));
}
});
};
export const useCreateOIDCConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
issuer,
configurationType,
discoveryURL,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret,
isActive,
orgSlug
}: {
issuer?: string;
configurationType: string;
discoveryURL?: string;
authorizationEndpoint?: string;
jwksUri?: string;
tokenEndpoint?: string;
userinfoEndpoint?: string;
clientId: string;
clientSecret: string;
isActive: boolean;
orgSlug: string;
allowedEmailDomains?: string;
}) => {
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
issuer,
configurationType,
discoveryURL,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret,
isActive,
orgSlug
});
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug));
}
});
};
@@ -0,0 +1,23 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { OIDCConfigData } from "./types";
export const oidcConfigKeys = {
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const
};
export const useGetOIDCConfig = (orgSlug: string) => {
return useQuery({
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug),
queryFn: async () => {
const { data } = await apiRequest.get<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
);
return data;
},
enabled: true
});
};
@@ -0,0 +1,15 @@
export type OIDCConfigData = {
id: string;
issuer: string;
authorizationEndpoint: string;
configurationType: string;
discoveryURL: string;
jwksUri: string;
tokenEndpoint: string;
userinfoEndpoint: string;
isActive: boolean;
orgId: string;
clientId: string;
clientSecret: string;
allowedEmailDomains?: string;
};
@@ -21,6 +21,7 @@ export type SubscriptionPlan = {
workspacesUsed: number;
environmentLimit: number;
samlSSO: boolean;
oidcSSO: boolean;
scim: boolean;
ldap: boolean;
groups: boolean;
+2 -1
View File
@@ -29,7 +29,8 @@ export type User = {
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
SAML = "saml",
OIDC = "oidc"
}
export type UserEnc = {
+4 -2
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { isLoggedIn } from "@app/reactQuery";
import { InitialStep, MFAStep, SAMLSSOStep } from "./components";
import { InitialStep, MFAStep, SSOStep } from "./components";
import { navigateUserToSelectOrg } from "./Login.utils";
export const Login = () => {
@@ -57,7 +57,9 @@ export const Login = () => {
/>
);
case 2:
return <SAMLSSOStep setStep={setStep} />;
return <SSOStep setStep={setStep} type="SAML" />;
case 3:
return <SSOStep setStep={setStep} type="OIDC" />;
default:
return <div />;
}
@@ -40,11 +40,13 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const { data: serverDetails } = useFetchServerStatus();
useEffect(() => {
if (serverDetails?.samlDefaultOrgSlug){
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${serverDetails?.samlDefaultOrgSlug}${callbackPort ? `?callback_port=${callbackPort}` : ""}`
router.push(redirectUrl);
}
if (serverDetails?.samlDefaultOrgSlug) {
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${
serverDetails?.samlDefaultOrgSlug
}${callbackPort ? `?callback_port=${callbackPort}` : ""}`;
router.push(redirectUrl);
}
}, [serverDetails?.samlDefaultOrgSlug]);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
@@ -217,6 +219,19 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
Continue with SAML
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with OIDC
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
@@ -1 +0,0 @@
export { SAMLSSOStep } from "./SAMLSSOStep";
@@ -5,9 +5,10 @@ import { Button, Input } from "@app/components/v2";
type Props = {
setStep: (step: number) => void;
type: "SAML" | "OIDC";
};
export const SAMLSSOStep = ({ setStep }: Props) => {
export const SSOStep = ({ setStep, type }: Props) => {
const [ssoIdentifier, setSSOIdentifier] = useState("");
const { t } = useTranslation();
@@ -16,21 +17,30 @@ export const SAMLSSOStep = ({ setStep }: Props) => {
const handleSubmission = (e: React.FormEvent) => {
e.preventDefault();
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
if (type === "SAML") {
window.open(
`/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
} else {
window.open(
`/api/v1/sso/oidc/login?orgSlug=${ssoIdentifier}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`
);
}
window.close();
};
return (
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 mb-8 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<p className="mx-auto mb-8 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
What&apos;s your organization slug?
</p>
<form onSubmit={handleSubmission}>
<div className="relative mx-auto flex max-h-24 w-1/4 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="relative mx-auto flex max-h-24 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={ssoIdentifier}
@@ -44,7 +54,7 @@ export const SAMLSSOStep = ({ setStep }: Props) => {
/>
</div>
</div>
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[20rem] items-center justify-center rounded-md text-center md:min-w-[22rem] lg:w-1/6">
<div className="mx-auto mt-4 flex w-full min-w-[20rem] items-center justify-center rounded-md text-center md:min-w-[22rem] lg:w-1/6">
<Button
type="submit"
colorSchema="primary"
@@ -52,7 +62,7 @@ export const SAMLSSOStep = ({ setStep }: Props) => {
isFullWidth
className="h-14"
>
Continue with SAML
Continue with {type}
</Button>
</div>
</form>
@@ -0,0 +1 @@
export { SSOStep } from "./SSOStep";
@@ -1,6 +1,6 @@
export { InitialStep } from "./InitialStep";
export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep";
export { SSOStep } from "./SSOStep";
// SSO-specific step
export { PasswordStep } from "./PasswordStep";
@@ -0,0 +1,404 @@
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,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetOIDCConfig } from "@app/hooks/api";
import { useCreateOIDCConfig, useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum ConfigurationType {
CUSTOM = "custom",
DISCOVERY_URL = "discoveryURL"
}
type Props = {
popUp: UsePopUpState<["addOIDC"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addOIDC"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addOIDC"]>, state?: boolean) => void;
};
const schema = z
.object({
configurationType: z.string(),
issuer: z.string().optional(),
discoveryURL: z.string().optional(),
authorizationEndpoint: z.string().optional(),
jwksUri: z.string().optional(),
tokenEndpoint: z.string().optional(),
userinfoEndpoint: z.string().optional(),
clientId: z.string().min(1),
clientSecret: z.string().min(1),
allowedEmailDomains: z.string().optional()
})
.superRefine((data, ctx) => {
if (data.configurationType === ConfigurationType.CUSTOM) {
if (!data.issuer) {
ctx.addIssue({
path: ["issuer"],
message: "Issuer is required",
code: z.ZodIssueCode.custom
});
}
if (!data.authorizationEndpoint) {
ctx.addIssue({
path: ["authorizationEndpoint"],
message: "Authorization endpoint is required",
code: z.ZodIssueCode.custom
});
}
if (!data.jwksUri) {
ctx.addIssue({
path: ["jwksUri"],
message: "JWKS URI is required",
code: z.ZodIssueCode.custom
});
}
if (!data.tokenEndpoint) {
ctx.addIssue({
path: ["tokenEndpoint"],
message: "Token endpoint is required",
code: z.ZodIssueCode.custom
});
}
if (!data.userinfoEndpoint) {
ctx.addIssue({
path: ["userinfoEndpoint"],
message: "Userinfo endpoint is required",
code: z.ZodIssueCode.custom
});
}
} else {
// eslint-disable-next-line no-lonely-if
if (!data.discoveryURL) {
ctx.addIssue({
path: ["discoveryURL"],
message: "Discovery URL is required",
code: z.ZodIssueCode.custom
});
}
}
});
export type OIDCFormData = z.infer<typeof schema>;
export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateOIDCConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateOIDCConfig();
const { data } = useGetOIDCConfig(currentOrg?.slug ?? "");
const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({
resolver: zodResolver(schema),
defaultValues: {
configurationType: ConfigurationType.DISCOVERY_URL
}
});
const [isClientIdFocused, setIsClientIdFocused] = useToggle();
const [isClientSecretFocused, setIsClientSecretFocused] = useToggle();
const configurationTypeValue = watch("configurationType");
useEffect(() => {
if (data) {
setValue("issuer", data.issuer);
setValue("authorizationEndpoint", data.authorizationEndpoint);
setValue("jwksUri", data.jwksUri);
setValue("tokenEndpoint", data.tokenEndpoint);
setValue("userinfoEndpoint", data.userinfoEndpoint);
setValue("discoveryURL", data.discoveryURL);
setValue("clientId", data.clientId);
setValue("clientSecret", data.clientSecret);
setValue("allowedEmailDomains", data.allowedEmailDomains);
setValue("configurationType", data.configurationType);
}
}, [data]);
const onOIDCModalSubmit = async ({
issuer,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
configurationType,
discoveryURL,
clientId,
clientSecret
}: OIDCFormData) => {
try {
if (!currentOrg) {
return;
}
if (!data) {
await createMutateAsync({
issuer,
configurationType,
discoveryURL,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret,
isActive: true,
orgSlug: currentOrg.slug
});
} else {
await updateMutateAsync({
issuer,
configurationType,
discoveryURL,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret,
isActive: true,
orgSlug: currentOrg.slug
});
}
handlePopUpClose("addOIDC");
createNotification({
text: `Successfully ${!data ? "added" : "updated"} OIDC SSO configuration`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${!data ? "add" : "update"} OIDC SSO configuration`,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.addOIDC?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addOIDC", isOpen);
reset();
}}
>
<ModalContent title="Manage OIDC configuration">
<form onSubmit={handleSubmit(onOIDCModalSubmit)}>
<Controller
control={control}
name="configurationType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full"
defaultValue="discoveryURL"
{...field}
onValueChange={(e) => onChange(e)}
>
<SelectItem value={ConfigurationType.DISCOVERY_URL}>Discovery URL</SelectItem>
<SelectItem value={ConfigurationType.CUSTOM}>Custom</SelectItem>
</Select>
</FormControl>
)}
/>
{configurationTypeValue === ConfigurationType.DISCOVERY_URL && (
<Controller
control={control}
name="discoveryURL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Discovery Document URL"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://accounts.google.com/.well-known/openid-configuration"
autoComplete="off"
/>
</FormControl>
)}
/>
)}
{configurationTypeValue === ConfigurationType.CUSTOM && (
<>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl label="Issuer" errorText={error?.message} isError={Boolean(error)}>
<Input
{...field}
placeholder="https://accounts.google.com"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="authorizationEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Authorization Endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://accounts.google.com/o/oauth2/v2/auth"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="tokenEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Token Endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://oauth2.googleapis.com/token"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="userinfoEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User info endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://openidconnect.googleapis.com/v1/userinfo"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="jwksUri"
render={({ field, fieldState: { error } }) => (
<FormControl label="JWKS URI" errorText={error?.message} isError={Boolean(error)}>
<Input
{...field}
placeholder="https://www.googleapis.com/oauth2/v3/certs"
autoComplete="off"
/>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="allowedEmailDomains"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Email Domains (defaults to any)"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="infisical.com, google.com" autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Client ID" errorText={error?.message} isError={Boolean(error)}>
<Input
placeholder="Client ID"
type={isClientIdFocused ? "text" : "password"}
onFocus={() => setIsClientIdFocused.on()}
{...field}
onBlur={() => {
field.onBlur();
setIsClientIdFocused.off();
}}
autoComplete="off"
className="bg-mineshaft-800"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="Client Secret"
type={isClientSecretFocused ? "text" : "password"}
autoComplete="off"
onFocus={() => setIsClientSecretFocused.on()}
onBlur={() => {
field.onBlur();
setIsClientSecretFocused.off();
}}
className="bg-mineshaft-800"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addOIDC")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -3,6 +3,7 @@ import { withPermission } from "@app/hoc";
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
import { OrgLDAPSection } from "./OrgLDAPSection";
import { OrgOIDCSection } from "./OrgOIDCSection";
import { OrgScimSection } from "./OrgSCIMSection";
import { OrgSSOSection } from "./OrgSSOSection";
@@ -12,6 +13,7 @@ export const OrgAuthTab = withPermission(
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgGeneralAuthSection />
<OrgSSOSection />
<OrgOIDCSection />
<OrgLDAPSection />
<OrgScimSection />
</div>
@@ -0,0 +1,118 @@
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { useGetOIDCConfig } from "@app/hooks/api";
import { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
import { usePopUp } from "@app/hooks/usePopUp";
import { OIDCModal } from "./OIDCModal";
export const OrgOIDCSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { data, isLoading } = useGetOIDCConfig(currentOrg?.slug ?? "");
const { mutateAsync } = useUpdateOIDCConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addOIDC",
"upgradePlan"
] as const);
const handleOIDCToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgSlug: currentOrg?.slug,
isActive: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} OIDC SSO`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${value ? "enable" : "disable"} OIDC SSO`,
type: "error"
});
}
};
const addOidcButtonClick = async () => {
if (subscription?.oidcSSO && currentOrg) {
handlePopUpOpen("addOIDC");
} else {
handlePopUpOpen("upgradePlan");
}
};
return (
<>
<hr className="border-mineshaft-600" />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">OIDC</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button
onClick={addOidcButtonClick}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Manage
</Button>
)}
</OrgPermissionCan>
)}
</div>
<p className="text-sm text-mineshaft-300">Manage OIDC authentication configuration</p>
</div>
{data && (
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable OIDC</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enable-oidc-sso"
onCheckedChange={(value) => handleOIDCToggle(value)}
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
)}
</div>
<p className="text-sm text-mineshaft-300">
Allow members to authenticate into Infisical with OIDC
</p>
</div>
)}
<OIDCModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use OIDC SSO if you switch to Infisical's Pro plan."
/>
</>
);
};
@@ -92,6 +92,10 @@ export const EmailConfirmationStep = ({
router.push(`/login/ldap?organizationSlug=${organizationSlug}`);
break;
}
case UserAliasType.OIDC: {
router.push(`/api/v1/sso/oidc/login?orgSlug=${organizationSlug}`);
break;
}
default: {
setStep(1);
break;
@@ -39,7 +39,8 @@ const formSchema = z.object({
signUpMode: z.nativeEnum(SignUpModes),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean(),
trustLdapEmails: z.boolean()
trustLdapEmails: z.boolean(),
trustOidcEmails: z.boolean()
});
type TDashboardForm = z.infer<typeof formSchema>;
@@ -60,7 +61,8 @@ export const AdminDashboardPage = () => {
signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled,
allowedSignUpDomain: config.allowedSignUpDomain,
trustSamlEmails: config.trustSamlEmails,
trustLdapEmails: config.trustLdapEmails
trustLdapEmails: config.trustLdapEmails,
trustOidcEmails: config.trustOidcEmails
}
});
@@ -84,13 +86,15 @@ export const AdminDashboardPage = () => {
const onFormSubmit = async (formData: TDashboardForm) => {
try {
const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails } = formData;
const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } =
formData;
await updateServerConfig({
allowSignUp: signUpMode !== SignUpModes.Disabled,
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
trustSamlEmails,
trustLdapEmails
trustLdapEmails,
trustOidcEmails
});
createNotification({
text: "Successfully changed sign up setting.",
@@ -190,9 +194,9 @@ export const AdminDashboardPage = () => {
<div className="mt-8 mb-8 flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Trust emails</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want Infisical to trust external emails from SAML/LDAP identity
providers. If set to false, then Infisical will prompt SAML/LDAP provisioned
users to verify their email upon their first login.
Select if you want Infisical to trust external emails from SAML/LDAP/OIDC
identity providers. If set to false, then Infisical will prompt SAML/LDAP/OIDC
provisioned users to verify their email upon their first login.
</div>
<Controller
control={control}
@@ -228,6 +232,23 @@ export const AdminDashboardPage = () => {
);
}}
/>
<Controller
control={control}
name="trustOidcEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-oidc-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust OIDC emails</p>
</Switch>
</FormControl>
);
}}
/>
</div>
<Button
type="submit"