diff --git a/messages/en-US.json b/messages/en-US.json
index 7d00c8105..505378b7f 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -890,7 +890,7 @@
"defaultMappingsRole": "Default Role Mapping",
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
"defaultMappingsOrg": "Default Organization Mapping",
- "defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.",
+ "defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsSubmit": "Save Default Mappings",
"orgPoliciesEdit": "Edit Organization Policy",
"org": "Organization",
@@ -1942,19 +1942,19 @@
"invalidValue": "Invalid value",
"idpTypeLabel": "Identity Provider Type",
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
- "roleMappingModeFixedRoles": "Fixed roles",
- "roleMappingModeMappingBuilder": "Mapping builder",
- "roleMappingModeRawExpression": "Raw expression",
+ "roleMappingModeFixedRoles": "Fixed Roles",
+ "roleMappingModeMappingBuilder": "Mapping Builder",
+ "roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
- "roleMappingClaimPath": "Claim path",
+ "roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
- "roleMappingMatchValue": "Match value",
- "roleMappingAssignRoles": "Assign roles",
- "roleMappingAddMappingRule": "Add mapping rule",
+ "roleMappingMatchValue": "Match Value",
+ "roleMappingAssignRoles": "Assign Roles",
+ "roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts
index 5b53f6820..0f0cc0cce 100644
--- a/server/routers/idp/createOidcIdp.ts
+++ b/server/routers/idp/createOidcIdp.ts
@@ -25,7 +25,8 @@ const bodySchema = z.strictObject({
namePath: z.string().optional(),
scopes: z.string().nonempty(),
autoProvision: z.boolean().optional(),
- tags: z.string().optional()
+ tags: z.string().optional(),
+ variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc")
});
export type CreateIdpResponse = {
@@ -77,7 +78,8 @@ export async function createOidcIdp(
namePath,
name,
autoProvision,
- tags
+ tags,
+ variant
} = parsedBody.data;
if (
@@ -121,7 +123,8 @@ export async function createOidcIdp(
scopes,
identifierPath,
emailPath,
- namePath
+ namePath,
+ variant
});
});
diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts
index fe32a8b08..905b32013 100644
--- a/server/routers/idp/updateOidcIdp.ts
+++ b/server/routers/idp/updateOidcIdp.ts
@@ -31,7 +31,8 @@ const bodySchema = z.strictObject({
autoProvision: z.boolean().optional(),
defaultRoleMapping: z.string().optional(),
defaultOrgMapping: z.string().optional(),
- tags: z.string().optional()
+ tags: z.string().optional(),
+ variant: z.enum(["oidc", "google", "azure"]).optional()
});
export type UpdateIdpResponse = {
@@ -96,7 +97,8 @@ export async function updateOidcIdp(
autoProvision,
defaultRoleMapping,
defaultOrgMapping,
- tags
+ tags,
+ variant
} = parsedBody.data;
if (process.env.IDENTITY_PROVIDER_MODE === "org") {
@@ -159,7 +161,8 @@ export async function updateOidcIdp(
scopes,
identifierPath,
emailPath,
- namePath
+ namePath,
+ variant
};
keysToUpdate = Object.keys(configData).filter(
diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx
index 9754b07e5..37cf400a5 100644
--- a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx
+++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx
@@ -448,16 +448,6 @@ export default function GeneralPage() {
-
-
-
- {t("redirectUrlAbout")}
-
-
- {t("redirectUrlAboutDescription")}
-
-
-
{/* IDP Type Indicator */}
@@ -843,29 +833,6 @@ export default function GeneralPage() {
className="space-y-4"
id="general-settings-form"
>
-
-
-
- {t("idpJmespathAbout")}
-
-
- {t(
- "idpJmespathAboutDescription"
- )}{" "}
-
- {t(
- "idpJmespathAboutDescriptionLink"
- )}{" "}
-
-
-
-
-
;
- interface ProviderTypeOption {
- id: "oidc" | "google" | "azure";
- title: string;
- description: string;
- icon?: React.ReactNode;
- }
-
- const providerTypes: ReadonlyArray = [
- {
- id: "oidc",
- title: "OAuth2/OIDC",
- description: t("idpOidcDescription")
- },
- {
- id: "google",
- title: t("idpGoogleTitle"),
- description: t("idpGoogleDescription"),
- icon: (
-
- )
- },
- {
- id: "azure",
- title: t("idpAzureTitle"),
- description: t("idpAzureDescription"),
- icon: (
-
- )
- }
- ];
-
const form = useForm({
resolver: zodResolver(createIdpFormSchema),
defaultValues: {
@@ -186,47 +142,6 @@ export default function Page() {
fetchRoles();
}, []);
- // Handle provider type changes and set defaults
- const handleProviderChange = (value: "oidc" | "google" | "azure") => {
- form.setValue("type", value);
-
- if (value === "google") {
- // Set Google defaults
- form.setValue(
- "authUrl",
- "https://accounts.google.com/o/oauth2/v2/auth"
- );
- form.setValue("tokenUrl", "https://oauth2.googleapis.com/token");
- form.setValue("identifierPath", "email");
- form.setValue("emailPath", "email");
- form.setValue("namePath", "name");
- form.setValue("scopes", "openid profile email");
- } else if (value === "azure") {
- // Set Azure Entra ID defaults (URLs will be constructed dynamically)
- form.setValue(
- "authUrl",
- "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/authorize"
- );
- form.setValue(
- "tokenUrl",
- "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/token"
- );
- form.setValue("identifierPath", "email");
- form.setValue("emailPath", "email");
- form.setValue("namePath", "name");
- form.setValue("scopes", "openid profile email");
- form.setValue("tenantId", "");
- } else {
- // Reset to OIDC defaults
- form.setValue("authUrl", "");
- form.setValue("tokenUrl", "");
- form.setValue("identifierPath", "sub");
- form.setValue("namePath", "name");
- form.setValue("emailPath", "email");
- form.setValue("scopes", "openid profile email");
- }
- };
-
async function onSubmit(data: CreateIdpFormValues) {
setCreateLoading(true);
@@ -304,6 +219,7 @@ export default function Page() {
}
const disabled = !isPaidUser(tierMatrix.orgOidc);
+ const templatesPaid = isPaidUser(tierMatrix.orgOidc);
return (
<>
@@ -336,23 +252,13 @@ export default function Page() {
-
-
-
- {t("idpType")}
-
-
-
{
- handleProviderChange(
- value as "oidc" | "google" | "azure"
- );
- }}
- cols={3}
- />
-
+ {
+ applyOidcIdpProviderType(form.setValue, next);
+ }}
+ />
-
-
-
-
- {t("idpOidcConfigureAlert")}
-
-
- {t("idpOidcConfigureAlertDescription")}
-
-
diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx
index a5ed14a6e..d02925976 100644
--- a/src/app/admin/idp/[idpId]/general/page.tsx
+++ b/src/app/admin/idp/[idpId]/general/page.tsx
@@ -25,7 +25,6 @@ import {
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
- SettingsSectionFooter,
SettingsSectionGrid
} from "@app/components/Settings";
import { formatAxiosError } from "@app/lib/api";
@@ -33,8 +32,6 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState, useEffect } from "react";
import { SwitchInput } from "@app/components/SwitchInput";
-import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
-import { InfoIcon, ExternalLink } from "lucide-react";
import {
InfoSection,
InfoSectionContent,
@@ -42,8 +39,7 @@ import {
InfoSectionTitle
} from "@app/components/InfoSection";
import CopyToClipboard from "@app/components/CopyToClipboard";
-import { Badge } from "@app/components/ui/badge";
-import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
+import IdpTypeBadge from "@app/components/IdpTypeBadge";
import { useTranslations } from "next-intl";
export default function GeneralPage() {
@@ -53,12 +49,12 @@ export default function GeneralPage() {
const { idpId } = useParams();
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
- const { isUnlocked } = useLicenseStatusContext();
+ const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
const t = useTranslations();
- const GeneralFormSchema = z.object({
+ const OidcFormSchema = z.object({
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
clientSecret: z
@@ -73,10 +69,46 @@ export default function GeneralPage() {
autoProvision: z.boolean().default(false)
});
- type GeneralFormValues = z.infer;
+ const GoogleFormSchema = z.object({
+ name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
+ clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
+ clientSecret: z
+ .string()
+ .min(1, { message: t("idpClientSecretRequired") }),
+ autoProvision: z.boolean().default(false)
+ });
- const form = useForm({
- resolver: zodResolver(GeneralFormSchema),
+ const AzureFormSchema = z.object({
+ name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
+ clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
+ clientSecret: z
+ .string()
+ .min(1, { message: t("idpClientSecretRequired") }),
+ tenantId: z.string().min(1, { message: t("idpTenantIdRequired") }),
+ autoProvision: z.boolean().default(false)
+ });
+
+ type OidcFormValues = z.infer;
+ type GoogleFormValues = z.infer;
+ type AzureFormValues = z.infer;
+ type GeneralFormValues =
+ | OidcFormValues
+ | GoogleFormValues
+ | AzureFormValues;
+
+ const getFormSchema = () => {
+ switch (variant) {
+ case "google":
+ return GoogleFormSchema;
+ case "azure":
+ return AzureFormSchema;
+ default:
+ return OidcFormSchema;
+ }
+ };
+
+ const form = useForm({
+ resolver: zodResolver(getFormSchema()) as never,
defaultValues: {
name: "",
clientId: "",
@@ -87,28 +119,60 @@ export default function GeneralPage() {
emailPath: "email",
namePath: "name",
scopes: "openid profile email",
- autoProvision: true
+ autoProvision: true,
+ tenantId: ""
}
});
+ useEffect(() => {
+ form.clearErrors();
+ }, [variant, form]);
+
useEffect(() => {
const loadIdp = async () => {
try {
const res = await api.get(`/idp/${idpId}`);
if (res.status === 200) {
const data = res.data.data;
- form.reset({
+ const idpVariant =
+ (data.idpOidcConfig?.variant as
+ | "oidc"
+ | "google"
+ | "azure") || "oidc";
+ setVariant(idpVariant);
+
+ let tenantId = "";
+ if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
+ const tenantMatch = data.idpOidcConfig.authUrl.match(
+ /login\.microsoftonline\.com\/([^/]+)\/oauth2/
+ );
+ if (tenantMatch) {
+ tenantId = tenantMatch[1];
+ }
+ }
+
+ const formData: Record = {
name: data.idp.name,
clientId: data.idpOidcConfig.clientId,
clientSecret: data.idpOidcConfig.clientSecret,
- authUrl: data.idpOidcConfig.authUrl,
- tokenUrl: data.idpOidcConfig.tokenUrl,
- identifierPath: data.idpOidcConfig.identifierPath,
- emailPath: data.idpOidcConfig.emailPath,
- namePath: data.idpOidcConfig.namePath,
- scopes: data.idpOidcConfig.scopes,
autoProvision: data.idp.autoProvision
- });
+ };
+
+ if (idpVariant === "oidc") {
+ formData.authUrl = data.idpOidcConfig.authUrl;
+ formData.tokenUrl = data.idpOidcConfig.tokenUrl;
+ formData.identifierPath =
+ data.idpOidcConfig.identifierPath;
+ formData.emailPath =
+ data.idpOidcConfig.emailPath ?? undefined;
+ formData.namePath =
+ data.idpOidcConfig.namePath ?? undefined;
+ formData.scopes = data.idpOidcConfig.scopes;
+ } else if (idpVariant === "azure") {
+ formData.tenantId = tenantId;
+ }
+
+ form.reset(formData as GeneralFormValues);
}
} catch (e) {
toast({
@@ -123,25 +187,76 @@ export default function GeneralPage() {
};
loadIdp();
- }, [idpId, api, form, router]);
+ }, [idpId]);
async function onSubmit(data: GeneralFormValues) {
setLoading(true);
try {
- const payload = {
+ const schema = getFormSchema();
+ const validationResult = schema.safeParse(data);
+
+ if (!validationResult.success) {
+ const errors = validationResult.error.flatten().fieldErrors;
+ Object.keys(errors).forEach((key) => {
+ const fieldName = key as keyof GeneralFormValues;
+ const errorMessage =
+ (errors as Record)[
+ key
+ ]?.[0] || t("invalidValue");
+ form.setError(fieldName, {
+ type: "manual",
+ message: errorMessage
+ });
+ });
+ setLoading(false);
+ return;
+ }
+
+ let payload: Record = {
name: data.name,
clientId: data.clientId,
clientSecret: data.clientSecret,
- authUrl: data.authUrl,
- tokenUrl: data.tokenUrl,
- identifierPath: data.identifierPath,
- emailPath: data.emailPath,
- namePath: data.namePath,
autoProvision: data.autoProvision,
- scopes: data.scopes
+ variant
};
+ if (variant === "oidc") {
+ const oidcData = data as OidcFormValues;
+ payload = {
+ ...payload,
+ authUrl: oidcData.authUrl,
+ tokenUrl: oidcData.tokenUrl,
+ identifierPath: oidcData.identifierPath,
+ emailPath: oidcData.emailPath ?? "",
+ namePath: oidcData.namePath ?? "",
+ scopes: oidcData.scopes
+ };
+ } else if (variant === "azure") {
+ const azureData = data as AzureFormValues;
+ const authUrl = `https://login.microsoftonline.com/${azureData.tenantId}/oauth2/v2.0/authorize`;
+ const tokenUrl = `https://login.microsoftonline.com/${azureData.tenantId}/oauth2/v2.0/token`;
+ payload = {
+ ...payload,
+ authUrl,
+ tokenUrl,
+ identifierPath: "email",
+ emailPath: "email",
+ namePath: "name",
+ scopes: "openid profile email"
+ };
+ } else if (variant === "google") {
+ payload = {
+ ...payload,
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
+ tokenUrl: "https://oauth2.googleapis.com/token",
+ identifierPath: "email",
+ emailPath: "email",
+ namePath: "name",
+ scopes: "openid profile email"
+ };
+ }
+
const res = await api.post(`/idp/${idpId}/oidc`, payload);
if (res.status === 200) {
@@ -190,6 +305,13 @@ export default function GeneralPage() {
+
+
+ {t("idpTypeLabel")}:
+
+
+
+
-
+
+
+
+ {t("idpAutoProvisionUsers")}
+
+
+ {t("idpAutoProvisionUsersDescription")}
+
+
+
+
+
+
+
+
+ {variant === "google" && (
- {t("idpOidcConfigure")}
+ {t("idpGoogleConfiguration")}
- {t("idpOidcConfigureDescription")}
+ {t("idpGoogleConfigurationDescription")}
@@ -294,7 +434,7 @@ export default function GeneralPage() {
{t(
- "idpClientIdDescription"
+ "idpGoogleClientIdDescription"
)}
@@ -318,49 +458,7 @@ export default function GeneralPage() {
{t(
- "idpClientSecretDescription"
- )}
-
-
-
- )}
- />
-
- (
-
-
- {t("idpAuthUrl")}
-
-
-
-
-
- {t(
- "idpAuthUrlDescription"
- )}
-
-
-
- )}
- />
-
- (
-
-
- {t("idpTokenUrl")}
-
-
-
-
-
- {t(
- "idpTokenUrlDescription"
+ "idpGoogleClientSecretDescription"
)}
@@ -372,14 +470,16 @@ export default function GeneralPage() {
+ )}
+ {variant === "azure" && (
- {t("idpToken")}
+ {t("idpAzureConfiguration")}
- {t("idpTokenDescription")}
+ {t("idpAzureConfigurationDescription")}
@@ -392,18 +492,18 @@ export default function GeneralPage() {
>
(
- {t("idpJmespathLabel")}
+ {t("idpTenantId")}
{t(
- "idpJmespathLabelDescription"
+ "idpAzureTenantIdDescription"
)}
@@ -413,20 +513,18 @@ export default function GeneralPage() {
(
- {t(
- "idpJmespathEmailPathOptional"
- )}
+ {t("idpClientId")}
{t(
- "idpJmespathEmailPathOptionalDescription"
+ "idpAzureClientIdDescription"
)}
@@ -436,43 +534,21 @@ export default function GeneralPage() {
(
- {t(
- "idpJmespathNamePathOptional"
- )}
+ {t("idpClientSecret")}
-
+
{t(
- "idpJmespathNamePathOptionalDescription"
- )}
-
-
-
- )}
- />
-
- (
-
-
- {t(
- "idpOidcConfigureScopes"
- )}
-
-
-
-
-
- {t(
- "idpOidcConfigureScopesDescription"
+ "idpAzureClientSecretDescription"
)}
@@ -484,15 +560,263 @@ export default function GeneralPage() {
-
+ )}
+
+ {variant === "oidc" && (
+
+
+
+
+ {t("idpOidcConfigure")}
+
+
+ {t("idpOidcConfigureDescription")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("idpToken")}
+
+
+ {t("idpTokenDescription")}
+
+
+
+
+
+
+
+
+
+
+ )}
{
+ form.handleSubmit(onSubmit)();
+ }}
>
{t("saveGeneralSettings")}
diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx
index 086074e2d..57ee3cf7b 100644
--- a/src/app/admin/idp/[idpId]/policies/page.tsx
+++ b/src/app/admin/idp/[idpId]/policies/page.tsx
@@ -575,7 +575,7 @@ export default function PoliciesPage() {
}
}}
>
-
+
{editingPolicy
diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx
index 91b55da23..40d4a3b32 100644
--- a/src/app/admin/idp/create/page.tsx
+++ b/src/app/admin/idp/create/page.tsx
@@ -1,5 +1,7 @@
"use client";
+import { OidcIdpProviderTypeSelect } from "@app/components/idp/OidcIdpProviderTypeSelect";
+import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import {
SettingsContainer,
SettingsSection,
@@ -20,70 +22,63 @@ import {
FormMessage
} from "@app/components/ui/form";
import HeaderTitle from "@app/components/SettingsSectionTitle";
-import { z } from "zod";
-import { createElement, useEffect, useState } from "react";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Input } from "@app/components/ui/input";
+import { SwitchInput } from "@app/components/SwitchInput";
+import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button";
-import { createApiClient, formatAxiosError } from "@app/lib/api";
+import { Input } from "@app/components/ui/input";
+import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
-import { useRouter } from "next/navigation";
-import { Checkbox } from "@app/components/ui/checkbox";
-import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
-import { InfoIcon, ExternalLink } from "lucide-react";
-import { StrategySelect } from "@app/components/StrategySelect";
-import { SwitchInput } from "@app/components/SwitchInput";
-import { Badge } from "@app/components/ui/badge";
-import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
+import { createApiClient, formatAxiosError } from "@app/lib/api";
+import { applyOidcIdpProviderType } from "@app/lib/idp/oidcIdpProviderDefaults";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
+import { InfoIcon } from "lucide-react";
import { useTranslations } from "next-intl";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
export default function Page() {
const { env } = useEnvContext();
const api = createApiClient({ env });
const router = useRouter();
const [createLoading, setCreateLoading] = useState(false);
- const { isUnlocked } = useLicenseStatusContext();
const t = useTranslations();
+ const { isPaidUser } = usePaidStatus();
+ const templatesPaid = isPaidUser(tierMatrix.orgOidc);
const createIdpFormSchema = z.object({
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
- type: z.enum(["oidc"]),
+ type: z.enum(["oidc", "google", "azure"]),
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
clientSecret: z
.string()
.min(1, { message: t("idpClientSecretRequired") }),
- authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }),
- tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }),
- identifierPath: z.string().min(1, { message: t("idpPathRequired") }),
+ authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }).optional(),
+ tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }).optional(),
+ identifierPath: z
+ .string()
+ .min(1, { message: t("idpPathRequired") })
+ .optional(),
emailPath: z.string().optional(),
namePath: z.string().optional(),
- scopes: z.string().min(1, { message: t("idpScopeRequired") }),
+ scopes: z
+ .string()
+ .min(1, { message: t("idpScopeRequired") })
+ .optional(),
+ tenantId: z.string().optional(),
autoProvision: z.boolean().default(false)
});
type CreateIdpFormValues = z.infer;
- interface ProviderTypeOption {
- id: "oidc";
- title: string;
- description: string;
- }
-
- const providerTypes: ReadonlyArray = [
- {
- id: "oidc",
- title: "OAuth2/OIDC",
- description: t("idpOidcDescription")
- }
- ];
-
const form = useForm({
resolver: zodResolver(createIdpFormSchema),
defaultValues: {
name: "",
- type: "oidc",
+ type: "oidc" as const,
clientId: "",
clientSecret: "",
authUrl: "",
@@ -92,25 +87,46 @@ export default function Page() {
namePath: "name",
emailPath: "email",
scopes: "openid profile email",
+ tenantId: "",
autoProvision: false
}
});
+ const watchedType = form.watch("type");
+
+ useEffect(() => {
+ if (
+ !templatesPaid &&
+ (watchedType === "google" || watchedType === "azure")
+ ) {
+ applyOidcIdpProviderType(form.setValue, "oidc");
+ }
+ }, [templatesPaid, watchedType, form.setValue]);
+
async function onSubmit(data: CreateIdpFormValues) {
setCreateLoading(true);
try {
+ let authUrl = data.authUrl;
+ let tokenUrl = data.tokenUrl;
+
+ if (data.type === "azure" && data.tenantId) {
+ authUrl = authUrl?.replace("{{TENANT_ID}}", data.tenantId);
+ tokenUrl = tokenUrl?.replace("{{TENANT_ID}}", data.tenantId);
+ }
+
const payload = {
name: data.name,
clientId: data.clientId,
clientSecret: data.clientSecret,
- authUrl: data.authUrl,
- tokenUrl: data.tokenUrl,
+ authUrl: authUrl,
+ tokenUrl: tokenUrl,
identifierPath: data.identifierPath,
emailPath: data.emailPath,
namePath: data.namePath,
autoProvision: data.autoProvision,
- scopes: data.scopes
+ scopes: data.scopes,
+ variant: data.type
};
const res = await api.put("/idp/oidc", payload);
@@ -150,6 +166,10 @@ export default function Page() {
+ {!templatesPaid ? (
+
+ ) : null}
+
@@ -161,6 +181,14 @@ export default function Page() {
+ {
+ applyOidcIdpProviderType(form.setValue, next);
+ }}
+ />
+
-
- {/* */}
- {/*
*/}
- {/* */}
- {/* {t("idpType")} */}
- {/* */}
- {/*
*/}
- {/* */}
- {/*
{ */}
- {/* form.setValue("type", value as "oidc"); */}
- {/* }} */}
- {/* cols={3} */}
- {/* /> */}
- {/* */}
- {form.watch("type") === "oidc" && (
+ {watchedType === "google" && (
+
+
+
+ {t("idpGoogleConfigurationTitle")}
+
+
+ {t("idpGoogleConfigurationDescription")}
+
+
+
+
+
+
+
+
+
+ )}
+
+ {watchedType === "azure" && (
+
+
+
+ {t("idpAzureConfigurationTitle")}
+
+
+ {t("idpAzureConfigurationDescription")}
+
+
+
+
+
+
+
+
+
+ )}
+
+ {watchedType === "oidc" && (
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index 44d85b99e..5f35ee4cd 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -12,6 +12,7 @@ import { authCookieHeader } from "@app/lib/api/cookies";
import { Layout } from "@app/components/Layout";
import { adminNavSections } from "../navigation";
import { pullEnv } from "@app/lib/pullEnv";
+import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider";
export const dynamic = "force-dynamic";
@@ -51,9 +52,15 @@ export default async function AdminLayout(props: LayoutProps) {
return (
-
- {props.children}
-
+
+
+ {props.children}
+
+
);
}
diff --git a/src/components/RoleMappingConfigFields.tsx b/src/components/RoleMappingConfigFields.tsx
index 4fe1a037b..deb2cc9ac 100644
--- a/src/components/RoleMappingConfigFields.tsx
+++ b/src/components/RoleMappingConfigFields.tsx
@@ -166,7 +166,7 @@ export default function RoleMappingConfigFields({
)}
{roleMappingMode === "mappingBuilder" && (
-
+
{t("roleMappingClaimPath")}
void;
+ templatesPaid: boolean;
+};
+
+export function OidcIdpProviderTypeSelect({
+ value,
+ onTypeChange,
+ templatesPaid
+}: Props) {
+ const t = useTranslations();
+
+ const options: ReadonlyArray
> = [
+ {
+ id: "oidc",
+ title: "OAuth2/OIDC",
+ description: t("idpOidcDescription")
+ },
+ {
+ id: "google",
+ title: t("idpGoogleTitle"),
+ description: t("idpGoogleDescription"),
+ disabled: !templatesPaid,
+ icon: (
+
+ )
+ },
+ {
+ id: "azure",
+ title: t("idpAzureTitle"),
+ description: t("idpAzureDescription"),
+ disabled: !templatesPaid,
+ icon: (
+
+ )
+ }
+ ];
+
+ return (
+
+ );
+}
diff --git a/src/lib/idp/oidcIdpProviderDefaults.ts b/src/lib/idp/oidcIdpProviderDefaults.ts
new file mode 100644
index 000000000..3608c6882
--- /dev/null
+++ b/src/lib/idp/oidcIdpProviderDefaults.ts
@@ -0,0 +1,46 @@
+import type { FieldValues, UseFormSetValue } from "react-hook-form";
+
+export type IdpOidcProviderType = "oidc" | "google" | "azure";
+
+export function applyOidcIdpProviderType(
+ setValue: UseFormSetValue,
+ provider: IdpOidcProviderType
+): void {
+ setValue("type" as never, provider as never);
+
+ if (provider === "google") {
+ setValue(
+ "authUrl" as never,
+ "https://accounts.google.com/o/oauth2/v2/auth" as never
+ );
+ setValue(
+ "tokenUrl" as never,
+ "https://oauth2.googleapis.com/token" as never
+ );
+ setValue("identifierPath" as never, "email" as never);
+ setValue("emailPath" as never, "email" as never);
+ setValue("namePath" as never, "name" as never);
+ setValue("scopes" as never, "openid profile email" as never);
+ } else if (provider === "azure") {
+ setValue(
+ "authUrl" as never,
+ "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/authorize" as never
+ );
+ setValue(
+ "tokenUrl" as never,
+ "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/token" as never
+ );
+ setValue("identifierPath" as never, "email" as never);
+ setValue("emailPath" as never, "email" as never);
+ setValue("namePath" as never, "name" as never);
+ setValue("scopes" as never, "openid profile email" as never);
+ setValue("tenantId" as never, "" as never);
+ } else {
+ setValue("authUrl" as never, "" as never);
+ setValue("tokenUrl" as never, "" as never);
+ setValue("identifierPath" as never, "sub" as never);
+ setValue("namePath" as never, "name" as never);
+ setValue("emailPath" as never, "email" as never);
+ setValue("scopes" as never, "openid profile email" as never);
+ }
+}