mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 13:26:16 +00:00
@@ -5,6 +5,7 @@ export type TServerConfig = {
|
||||
isMigrationModeOn?: boolean;
|
||||
trustSamlEmails: boolean;
|
||||
trustLdapEmails: boolean;
|
||||
trustOidcEmails: boolean;
|
||||
isSecretScanningDisabled: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -29,7 +29,8 @@ export type User = {
|
||||
|
||||
export enum UserAliasType {
|
||||
LDAP = "ldap",
|
||||
SAML = "saml"
|
||||
SAML = "saml",
|
||||
OIDC = "oidc"
|
||||
}
|
||||
|
||||
export type UserEnc = {
|
||||
|
||||
@@ -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";
|
||||
+20
-10
@@ -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'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"
|
||||
|
||||
Reference in New Issue
Block a user