add option to pre provision idp user

This commit is contained in:
miloschwartz
2025-04-23 15:44:27 -04:00
parent 960eb34c7d
commit 97af632c61
3 changed files with 840 additions and 470 deletions

View File

@@ -38,14 +38,14 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { Checkbox } from "@app/components/ui/checkbox"; import { Checkbox } from "@app/components/ui/checkbox";
import { ListIdpsResponse } from "@server/routers/idp";
type UserType = "internal" | "external"; type UserType = "internal" | "oidc";
interface UserTypeOption { interface UserTypeOption {
id: UserType; id: UserType;
@@ -53,12 +53,39 @@ interface UserTypeOption {
description: string; description: string;
} }
const formSchema = z.object({ interface IdpOption {
idpId: number;
name: string;
type: string;
}
const internalFormSchema = z.object({
email: z.string().email({ message: "Invalid email address" }), email: z.string().email({ message: "Invalid email address" }),
validForHours: z.string().min(1, { message: "Please select a duration" }), validForHours: z.string().min(1, { message: "Please select a duration" }),
roleId: z.string().min(1, { message: "Please select a role" }) roleId: z.string().min(1, { message: "Please select a role" })
}); });
const externalFormSchema = z.object({
username: z.string().min(1, { message: "Username is required" }),
email: z
.string()
.email({ message: "Invalid email address" })
.optional()
.or(z.literal("")),
name: z.string().optional(),
roleId: z.string().min(1, { message: "Please select a role" }),
idpId: z.string().min(1, { message: "Please select an identity provider" })
});
const formatIdpType = (type: string) => {
switch (type.toLowerCase()) {
case "oidc":
return "Generic OAuth2/OIDC provider.";
default:
return type;
}
};
export default function Page() { export default function Page() {
const { orgId } = useParams(); const { orgId } = useParams();
const router = useRouter(); const router = useRouter();
@@ -70,7 +97,10 @@ export default function Page() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [expiresInDays, setExpiresInDays] = useState(1); const [expiresInDays, setExpiresInDays] = useState(1);
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
const [idps, setIdps] = useState<IdpOption[]>([]);
const [sendEmail, setSendEmail] = useState(env.email.emailEnabled); const [sendEmail, setSendEmail] = useState(env.email.emailEnabled);
const [selectedIdp, setSelectedIdp] = useState<IdpOption | null>(null);
const [dataLoaded, setDataLoaded] = useState(false);
const validFor = [ const validFor = [
{ hours: 24, name: "1 day" }, { hours: 24, name: "1 day" },
@@ -82,8 +112,8 @@ export default function Page() {
{ hours: 168, name: "7 days" } { hours: 168, name: "7 days" }
]; ];
const form = useForm<z.infer<typeof formSchema>>({ const internalForm = useForm<z.infer<typeof internalFormSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(internalFormSchema),
defaultValues: { defaultValues: {
email: "", email: "",
validForHours: "72", validForHours: "72",
@@ -91,17 +121,30 @@ export default function Page() {
} }
}); });
const externalForm = useForm<z.infer<typeof externalFormSchema>>({
resolver: zodResolver(externalFormSchema),
defaultValues: {
username: "",
email: "",
name: "",
roleId: "",
idpId: ""
}
});
useEffect(() => { useEffect(() => {
if (userType === "internal") { if (userType === "internal") {
setSendEmail(env.email.emailEnabled); setSendEmail(env.email.emailEnabled);
form.reset(); internalForm.reset();
setInviteLink(null); setInviteLink(null);
setExpiresInDays(1); setExpiresInDays(1);
} else if (userType === "oidc") {
externalForm.reset();
} }
}, [userType, env.email.emailEnabled, form]); }, [userType, env.email.emailEnabled, internalForm, externalForm]);
useEffect(() => { useEffect(() => {
if (userType !== "internal") { if (!userType) {
return; return;
} }
@@ -122,13 +165,43 @@ export default function Page() {
if (res?.status === 200) { if (res?.status === 200) {
setRoles(res.data.data.roles); setRoles(res.data.data.roles);
if (userType === "internal") {
setDataLoaded(true);
}
} }
} }
async function fetchIdps() {
const res = await api
.get<AxiosResponse<ListIdpsResponse>>("/idp")
.catch((e) => {
console.error(e);
toast({
variant: "destructive",
title: "Failed to fetch identity providers",
description: formatAxiosError(
e,
"An error occurred while fetching identity providers"
)
});
});
if (res?.status === 200) {
setIdps(res.data.data.idps);
setDataLoaded(true);
}
}
setDataLoaded(false);
fetchRoles(); fetchRoles();
if (userType !== "internal") {
fetchIdps();
}
}, [userType]); }, [userType]);
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmitInternal(
values: z.infer<typeof internalFormSchema>
) {
setLoading(true); setLoading(true);
const res = await api const res = await api
@@ -175,6 +248,43 @@ export default function Page() {
setLoading(false); setLoading(false);
} }
async function onSubmitExternal(
values: z.infer<typeof externalFormSchema>
) {
setLoading(true);
const res = await api
.put(`/org/${orgId}/user`, {
username: values.username,
email: values.email,
name: values.name,
type: "oidc",
idpId: parseInt(values.idpId),
roleId: parseInt(values.roleId)
})
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to create user",
description: formatAxiosError(
e,
"An error occurred while creating the user"
)
});
});
if (res && res.status === 201) {
toast({
variant: "default",
title: "User created",
description: "The user has been successfully created."
});
router.push(`/${orgId}/settings/access/users`);
}
setLoading(false);
}
const userTypes: ReadonlyArray<UserTypeOption> = [ const userTypes: ReadonlyArray<UserTypeOption> = [
{ {
id: "internal", id: "internal",
@@ -182,10 +292,9 @@ export default function Page() {
description: "Invite a user to join your organization directly." description: "Invite a user to join your organization directly."
}, },
{ {
id: "external", id: "oidc",
title: "External User", title: "External User",
description: description: "Create a user with an external identity provider."
"Provision a user with an external identity provider (IdP)."
} }
]; ];
@@ -223,196 +332,434 @@ export default function Page() {
defaultValue={userType || undefined} defaultValue={userType || undefined}
onChange={(value) => { onChange={(value) => {
setUserType(value as UserType); setUserType(value as UserType);
if (value === "internal") {
internalForm.reset();
} else if (value === "oidc") {
externalForm.reset();
setSelectedIdp(null);
}
}} }}
cols={2} cols={2}
/> />
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
{userType === "internal" && ( {userType === "internal" && dataLoaded && (
<SettingsSection> <>
<SettingsSectionHeader> <SettingsSection>
<SettingsSectionTitle> <SettingsSectionHeader>
User Information <SettingsSectionTitle>
</SettingsSectionTitle> User Information
<SettingsSectionDescription> </SettingsSectionTitle>
Enter the details for the new user <SettingsSectionDescription>
</SettingsSectionDescription> Enter the details for the new user
</SettingsSectionHeader> </SettingsSectionDescription>
<SettingsSectionBody> </SettingsSectionHeader>
<SettingsSectionForm> <SettingsSectionBody>
<Form {...form}> <SettingsSectionForm>
<form <Form {...internalForm}>
onSubmit={form.handleSubmit( <form
onSubmit onSubmit={internalForm.handleSubmit(
)} onSubmitInternal
className="space-y-4" )}
id="invite-user-form" className="space-y-4"
> id="create-user-form"
>
<FormField
control={
internalForm.control
}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
Email
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Send invite email to
user
</label>
</div>
)}
<FormField
control={
internalForm.control
}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
Valid For
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select duration" />
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
internalForm.control
}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>
Role
</FormLabel>
<Select
onValueChange={
field.onChange
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map(
(
role
) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{
role.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{inviteLink && (
<div className="max-w-md space-y-4">
{sendEmail && (
<p>
An email has
been sent to the
user with the
access link
below. They must
access the link
to accept the
invitation.
</p>
)}
{!sendEmail && (
<p>
The user has
been invited.
They must access
the link below
to accept the
invitation.
</p>
)}
<p>
The invite will
expire in{" "}
<b>
{expiresInDays}{" "}
{expiresInDays ===
1
? "day"
: "days"}
</b>
.
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</>
)}
{userType !== "internal" && dataLoaded && (
<>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Identity Provider
</SettingsSectionTitle>
<SettingsSectionDescription>
Select the identity provider for the
external user
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{idps.length === 0 ? (
<p className="text-muted-foreground">
No identity providers are
configured. Please configure an
identity provider before creating
external users.
</p>
) : (
<Form {...externalForm}>
<FormField <FormField
control={form.control} control={externalForm.control}
name="email" name="idpId"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <StrategySelect
Email options={idps.map(
</FormLabel> (idp) => ({
<FormControl> id: idp.idpId.toString(),
<Input {...field} /> title: idp.name,
</FormControl> description:
formatIdpType(
idp.type
)
})
)}
defaultValue={
field.value
}
onChange={(
value
) => {
field.onChange(
value
);
const idp =
idps.find(
(idp) =>
idp.idpId.toString() ===
value
);
setSelectedIdp(
idp || null
);
}}
cols={3}
/>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</Form>
)}
</SettingsSectionBody>
</SettingsSection>
{env.email.emailEnabled && ( {idps.length > 0 && (
<div className="flex items-center space-x-2"> <SettingsSection>
<Checkbox <SettingsSectionHeader>
id="send-email" <SettingsSectionTitle>
checked={sendEmail} User Information
onCheckedChange={(e) => </SettingsSectionTitle>
setSendEmail( <SettingsSectionDescription>
e as boolean Enter the details for the new user
) </SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...externalForm}>
<form
onSubmit={externalForm.handleSubmit(
onSubmitExternal
)}
className="space-y-4"
id="create-user-form"
>
<FormField
control={
externalForm.control
} }
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>
Username
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<p className="text-sm text-muted-foreground mt-1">
This must
match the
unique
username
that exists
in the
selected
identity
provider.
</p>
<FormMessage />
</FormItem>
)}
/> />
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Send invite email to
user
</label>
</div>
)}
<FormField <FormField
control={form.control} control={
name="roleId" externalForm.control
render={({ field }) => ( }
<FormItem> name="email"
<FormLabel> render={({ field }) => (
Role <FormItem>
</FormLabel> <FormLabel>
<Select Email
onValueChange={ (Optional)
field.onChange </FormLabel>
} <FormControl>
> <Input
<FormControl> {...field}
<SelectTrigger> />
<SelectValue placeholder="Select role" /> </FormControl>
</SelectTrigger> <FormMessage />
</FormControl> </FormItem>
<SelectContent> )}
{roles.map(
(role) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{
role.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
Valid For
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={field.value.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select duration" />
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{inviteLink && (
<div className="space-y-4">
{sendEmail && (
<p>
An email has been
sent to the user
with the access link
below. They must
access the link to
accept the
invitation.
</p>
)}
{!sendEmail && (
<p>
The user has been
invited. They must
access the link
below to accept the
invitation.
</p>
)}
<p>
The invite will expire
in{" "}
<b>
{expiresInDays}{" "}
{expiresInDays === 1
? "day"
: "days"}
</b>
.
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/> />
</div>
)} <FormField
</form> control={
</Form> externalForm.control
</SettingsSectionForm> }
</SettingsSectionBody> name="name"
</SettingsSection> render={({ field }) => (
<FormItem>
<FormLabel>
Name
(Optional)
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
externalForm.control
}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>
Role
</FormLabel>
<Select
onValueChange={
field.onChange
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map(
(
role
) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{
role.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
</>
)} )}
</SettingsContainer> </SettingsContainer>
@@ -426,12 +773,15 @@ export default function Page() {
> >
Cancel Cancel
</Button> </Button>
{userType === "internal" && ( {userType && dataLoaded && (
<Button <Button
type="submit" type="submit"
form="invite-user-form" form="create-user-form"
loading={loading} loading={loading}
disabled={inviteLink !== null || loading} disabled={
loading ||
(userType === "internal" && inviteLink !== null)
}
> >
Create User Create User
</Button> </Button>

View File

@@ -44,6 +44,7 @@ export default function IdpTable({ idps }: Props) {
title: "Success", title: "Success",
description: "Identity provider deleted successfully" description: "Identity provider deleted successfully"
}); });
setIsDeleteModalOpen(false);
router.refresh(); router.refresh();
} catch (e) { } catch (e) {
toast({ toast({

View File

@@ -162,104 +162,42 @@ export default function GeneralPage() {
} }
return ( return (
<SettingsContainer> <>
<SettingsSection> <SettingsContainer>
<SettingsSectionHeader>
<SettingsSectionTitle>
General Information
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the basic information for your identity
provider
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>Redirect URL</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard text={redirectUrl} />
</InfoSectionContent>
</InfoSection>
</InfoSections>
<Alert variant="neutral" className="">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About Redirect URL
</AlertTitle>
<AlertDescription>
This is the URL to which users will be redirected
after authentication. You need to configure this URL
in your identity provider settings.
</AlertDescription>
</Alert>
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="general-settings-form"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
A display name for this identity
provider
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-start mb-0">
<SwitchInput
id="auto-provision-toggle"
label="Auto Provision Users"
defaultChecked={form.getValues(
"autoProvision"
)}
disabled={true}
onCheckedChange={(checked) => {
form.setValue(
"autoProvision",
checked
);
}}
/>
<Badge className="ml-2">Enterprise</Badge>
</div>
<span className="text-sm text-muted-foreground">
When enabled, users will be automatically
created in the system upon first login with
the ability to map users to roles and
organizations.
</span>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
<SettingsSectionGrid cols={2}>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
OAuth2/OIDC Configuration General Information
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
Configure the OAuth2/OIDC provider endpoints and Configure the basic information for your identity
credentials provider
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
Redirect URL
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard text={redirectUrl} />
</InfoSectionContent>
</InfoSection>
</InfoSections>
<Alert variant="neutral" className="">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About Redirect URL
</AlertTitle>
<AlertDescription>
This is the URL to which users will be
redirected after authentication. You need to
configure this URL in your identity provider
settings.
</AlertDescription>
</Alert>
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}> <Form {...form}>
<form <form
@@ -269,220 +207,301 @@ export default function GeneralPage() {
> >
<FormField <FormField
control={form.control} control={form.control}
name="clientId" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Client ID</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The OAuth2 client ID from A display name for this
your identity provider identity provider
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <div className="flex items-start mb-0">
control={form.control} <SwitchInput
name="clientSecret" id="auto-provision-toggle"
render={({ field }) => ( label="Auto Provision Users"
<FormItem> defaultChecked={form.getValues(
<FormLabel> "autoProvision"
Client Secret )}
</FormLabel> disabled={true}
<FormControl> onCheckedChange={(checked) => {
<Input form.setValue(
type="password" "autoProvision",
{...field} checked
/> );
</FormControl> }}
<FormDescription> />
The OAuth2 client secret <Badge className="ml-2">
from your identity provider Enterprise
</FormDescription> </Badge>
<FormMessage /> </div>
</FormItem> <span className="text-sm text-muted-foreground">
)} When enabled, users will be
/> automatically created in the system upon
first login with the ability to map
<FormField users to roles and organizations.
control={form.control} </span>
name="authUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
Authorization URL
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 authorization
endpoint URL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tokenUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Token URL</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 token endpoint
URL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
<SettingsSection> <SettingsSectionGrid cols={2}>
<SettingsSectionHeader> <SettingsSection>
<SettingsSectionTitle> <SettingsSectionHeader>
Token Configuration <SettingsSectionTitle>
</SettingsSectionTitle> OAuth2/OIDC Configuration
<SettingsSectionDescription> </SettingsSectionTitle>
Configure how to extract user information from the <SettingsSectionDescription>
ID token Configure the OAuth2/OIDC provider endpoints and
</SettingsSectionDescription> credentials
</SettingsSectionHeader> </SettingsSectionDescription>
<SettingsSectionBody> </SettingsSectionHeader>
<SettingsSectionForm> <SettingsSectionBody>
<Form {...form}> <SettingsSectionForm>
<form <Form {...form}>
onSubmit={form.handleSubmit(onSubmit)} <form
className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}
id="general-settings-form" className="space-y-4"
> id="general-settings-form"
<Alert variant="neutral"> >
<InfoIcon className="h-4 w-4" /> <FormField
<AlertTitle className="font-semibold"> control={form.control}
About JMESPath name="clientId"
</AlertTitle> render={({ field }) => (
<AlertDescription> <FormItem>
The paths below use JMESPath syntax <FormLabel>
to extract values from the ID token. Client ID
<a </FormLabel>
href="https://jmespath.org" <FormControl>
target="_blank" <Input {...field} />
rel="noopener noreferrer" </FormControl>
className="text-primary hover:underline inline-flex items-center" <FormDescription>
> The OAuth2 client ID
Learn more about JMESPath{" "} from your identity
<ExternalLink className="ml-1 h-4 w-4" /> provider
</a> </FormDescription>
</AlertDescription> <FormMessage />
</Alert> </FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="identifierPath" name="clientSecret"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Identifier Path Client Secret
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input
</FormControl> type="password"
<FormDescription> {...field}
The JMESPath to the user />
identifier in the ID token </FormControl>
</FormDescription> <FormDescription>
<FormMessage /> The OAuth2 client secret
</FormItem> from your identity
)} provider
/> </FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="emailPath" name="authUrl"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Email Path (Optional) Authorization URL
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the user's The OAuth2 authorization
email in the ID token endpoint URL
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="namePath" name="tokenUrl"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Name Path (Optional) Token URL
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the user's The OAuth2 token
name in the ID token endpoint URL
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
<FormField <SettingsSection>
control={form.control} <SettingsSectionHeader>
name="scopes" <SettingsSectionTitle>
render={({ field }) => ( Token Configuration
<FormItem> </SettingsSectionTitle>
<FormLabel>Scopes</FormLabel> <SettingsSectionDescription>
<FormControl> Configure how to extract user information from
<Input {...field} /> the ID token
</FormControl> </SettingsSectionDescription>
<FormDescription> </SettingsSectionHeader>
Space-separated list of <SettingsSectionBody>
OAuth2 scopes to request <SettingsSectionForm>
</FormDescription> <Form {...form}>
<FormMessage /> <form
</FormItem> onSubmit={form.handleSubmit(onSubmit)}
)} className="space-y-4"
/> id="general-settings-form"
</form> >
</Form> <Alert variant="neutral">
</SettingsSectionForm> <InfoIcon className="h-4 w-4" />
</SettingsSectionBody> <AlertTitle className="font-semibold">
About JMESPath
</AlertTitle>
<AlertDescription>
The paths below use JMESPath
syntax to extract values from
the ID token.
<a
href="https://jmespath.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Learn more about JMESPath{" "}
<ExternalLink className="ml-1 h-4 w-4" />
</a>
</AlertDescription>
</Alert>
<SettingsSectionFooter> <FormField
<Button control={form.control}
type="submit" name="identifierPath"
form="general-settings-form" render={({ field }) => (
loading={loading} <FormItem>
disabled={loading} <FormLabel>
> Identifier Path
Save Settings </FormLabel>
</Button> <FormControl>
</SettingsSectionFooter> <Input {...field} />
</SettingsSection> </FormControl>
</SettingsSectionGrid> <FormDescription>
</SettingsContainer> The JMESPath to the user
identifier in the ID
token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="emailPath"
render={({ field }) => (
<FormItem>
<FormLabel>
Email Path (Optional)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's email in the ID
token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="namePath"
render={({ field }) => (
<FormItem>
<FormLabel>
Name Path (Optional)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's name in the ID
token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="scopes"
render={({ field }) => (
<FormItem>
<FormLabel>
Scopes
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Space-separated list of
OAuth2 scopes to request
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</SettingsSectionGrid>
</SettingsContainer>
<div className="flex justify-end mt-8">
<Button
type="submit"
form="general-settings-form"
loading={loading}
disabled={loading}
>
Save Settings
</Button>
</div>
</>
); );
} }