diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx
index 15f299b47d..c4a242a5c3 100644
--- a/ui/components/providers/workflow/forms/connect-account-form.tsx
+++ b/ui/components/providers/workflow/forms/connect-account-form.tsx
@@ -121,6 +121,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
label: "Team ID",
placeholder: "e.g. team_xxxxxxxxxxxxxxxxxxxxxxxx",
};
+ case "okta":
+ return {
+ label: "Org Domain",
+ placeholder: "e.g. your-org.okta.com",
+ };
default:
return {
label: "Provider UID",
diff --git a/ui/components/providers/workflow/forms/via-credentials/okta-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/okta-credentials-form.tsx
new file mode 100644
index 0000000000..42aa16124b
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-credentials/okta-credentials-form.tsx
@@ -0,0 +1,53 @@
+import { Control } from "react-hook-form";
+
+import {
+ WizardInputField,
+ WizardTextareaField,
+} from "@/components/providers/workflow/forms/fields";
+import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
+import { OktaCredentials } from "@/types";
+
+export const OktaCredentialsForm = ({
+ control,
+}: {
+ control: Control
;
+}) => {
+ return (
+ <>
+
+
+ Connect via OAuth 2.0 Private Key JWT
+
+
+ Provide the Client ID and PEM-encoded private key of an Okta API
+ Services app whose matching public key (JWK) is registered on the
+ service app.
+
+
+
+
+
+ The private key is sent over TLS and stored as a secret in the backend.
+ You can rotate or revoke the public key from the Okta admin console at
+ any time.
+
+ >
+ );
+};
diff --git a/ui/components/ui/entities/get-provider-logo.tsx b/ui/components/ui/entities/get-provider-logo.tsx
index 7b8218c596..7481d0a8ed 100644
--- a/ui/components/ui/entities/get-provider-logo.tsx
+++ b/ui/components/ui/entities/get-provider-logo.tsx
@@ -11,6 +11,7 @@ import {
KS8ProviderBadge,
M365ProviderBadge,
MongoDBAtlasProviderBadge,
+ OktaProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
VercelProviderBadge,
@@ -49,6 +50,8 @@ export const getProviderLogo = (provider: ProviderType) => {
return ;
case "vercel":
return ;
+ case "okta":
+ return ;
default:
return null;
}
@@ -86,6 +89,8 @@ export const getProviderName = (provider: ProviderType): string => {
return "OpenStack";
case "vercel":
return "Vercel";
+ case "okta":
+ return "Okta";
default:
return "Unknown Provider";
}
diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts
index 27f221fc2d..71cb139e5d 100644
--- a/ui/hooks/use-credentials-form.ts
+++ b/ui/hooks/use-credentials-form.ts
@@ -248,6 +248,12 @@ export const useCredentialsForm = ({
...baseDefaults,
[ProviderCredentialFields.VERCEL_API_TOKEN]: "",
};
+ case "okta":
+ return {
+ ...baseDefaults,
+ [ProviderCredentialFields.OKTA_CLIENT_ID]: "",
+ [ProviderCredentialFields.OKTA_PRIVATE_KEY]: "",
+ };
default:
return baseDefaults;
}
diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts
index e968bf4ab4..8c6105e4ec 100644
--- a/ui/lib/external-urls.ts
+++ b/ui/lib/external-urls.ts
@@ -100,6 +100,11 @@ export const getProviderHelpText = (provider: string) => {
text: "Need help connecting your Vercel team?",
link: "https://goto.prowler.com/provider-vercel",
};
+ case "okta":
+ return {
+ text: "Need help connecting your Okta organization?",
+ link: "https://goto.prowler.com/provider-okta",
+ };
default:
return {
text: "How to setup a provider?",
diff --git a/ui/lib/provider-credentials/build-crendentials.ts b/ui/lib/provider-credentials/build-credentials.ts
similarity index 96%
rename from ui/lib/provider-credentials/build-crendentials.ts
rename to ui/lib/provider-credentials/build-credentials.ts
index d52e57cdfe..ae8c26e02c 100644
--- a/ui/lib/provider-credentials/build-crendentials.ts
+++ b/ui/lib/provider-credentials/build-credentials.ts
@@ -260,6 +260,20 @@ export const buildVercelSecret = (formData: FormData) => {
return filterEmptyValues(secret);
};
+export const buildOktaSecret = (formData: FormData) => {
+ const secret = {
+ [ProviderCredentialFields.OKTA_CLIENT_ID]: getFormValue(
+ formData,
+ ProviderCredentialFields.OKTA_CLIENT_ID,
+ ),
+ [ProviderCredentialFields.OKTA_PRIVATE_KEY]: getFormValue(
+ formData,
+ ProviderCredentialFields.OKTA_PRIVATE_KEY,
+ ),
+ };
+ return filterEmptyValues(secret);
+};
+
export const buildOpenStackSecret = (formData: FormData) => {
const secret = {
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: getFormValue(
@@ -513,6 +527,10 @@ export const buildSecretConfig = (
secretType: "static",
secret: buildVercelSecret(formData),
}),
+ okta: () => ({
+ secretType: "static",
+ secret: buildOktaSecret(formData),
+ }),
};
const builder = secretBuilders[providerType];
diff --git a/ui/lib/provider-credentials/provider-credential-fields.ts b/ui/lib/provider-credentials/provider-credential-fields.ts
index 96ea5de2ae..302f62bf33 100644
--- a/ui/lib/provider-credentials/provider-credential-fields.ts
+++ b/ui/lib/provider-credentials/provider-credential-fields.ts
@@ -91,6 +91,10 @@ export const ProviderCredentialFields = {
// Vercel fields
VERCEL_API_TOKEN: "api_token",
+
+ // Okta fields
+ OKTA_CLIENT_ID: "okta_client_id",
+ OKTA_PRIVATE_KEY: "okta_private_key",
} as const;
// Type for credential field values
@@ -150,6 +154,8 @@ export const ErrorPointers = {
"/data/attributes/secret/credentials_content",
GOOGLEWORKSPACE_DELEGATED_USER: "/data/attributes/secret/delegated_user",
VERCEL_API_TOKEN: "/data/attributes/secret/api_token",
+ OKTA_CLIENT_ID: "/data/attributes/secret/okta_client_id",
+ OKTA_PRIVATE_KEY: "/data/attributes/secret/okta_private_key",
} as const;
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
diff --git a/ui/types/components.ts b/ui/types/components.ts
index 30da635fd2..ba8cb53144 100644
--- a/ui/types/components.ts
+++ b/ui/types/components.ts
@@ -389,6 +389,12 @@ export type VercelCredentials = {
[ProviderCredentialFields.PROVIDER_ID]: string;
};
+export type OktaCredentials = {
+ [ProviderCredentialFields.OKTA_CLIENT_ID]: string;
+ [ProviderCredentialFields.OKTA_PRIVATE_KEY]: string;
+ [ProviderCredentialFields.PROVIDER_ID]: string;
+};
+
export type CredentialsFormSchema =
| AWSCredentials
| AWSCredentialsRole
@@ -406,7 +412,8 @@ export type CredentialsFormSchema =
| CloudflareCredentials
| OpenStackCredentials
| GoogleWorkspaceCredentials
- | VercelCredentials;
+ | VercelCredentials
+ | OktaCredentials;
export interface SearchParamsProps {
[key: string]: string | string[] | undefined;
diff --git a/ui/types/formSchemas.test.ts b/ui/types/formSchemas.test.ts
index 4c98068a53..e4fa3ea663 100644
--- a/ui/types/formSchemas.test.ts
+++ b/ui/types/formSchemas.test.ts
@@ -2,7 +2,11 @@ import { describe, expect, it } from "vitest";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
-import { addCredentialsRoleFormSchema } from "./formSchemas";
+import {
+ addCredentialsFormSchema,
+ addCredentialsRoleFormSchema,
+ addProviderFormSchema,
+} from "./formSchemas";
const BASE_AWS_ROLE_VALUES = {
[ProviderCredentialFields.PROVIDER_ID]: "provider-123",
@@ -46,3 +50,103 @@ describe("addCredentialsRoleFormSchema", () => {
);
});
});
+
+describe("addProviderFormSchema - okta", () => {
+ const validUidFixtures = [
+ "acme.okta.com",
+ "acme.oktapreview.com",
+ "acme.okta-emea.com",
+ "agency.okta-gov.com",
+ "agency.okta.mil",
+ "agency.okta-miltest.com",
+ "agency.trex-govcloud.com",
+ "Acme.okta.com",
+ " ACME.OKTA.COM ",
+ "Agency.Okta-Gov.com",
+ ];
+
+ it.each(validUidFixtures)("accepts okta-managed org domain %s", (uid) => {
+ const result = addProviderFormSchema.safeParse({
+ providerType: "okta",
+ providerUid: uid,
+ providerAlias: "okta-test",
+ });
+
+ expect(result.success).toBe(true);
+ });
+
+ it.each([
+ ["Acme.okta.com", "acme.okta.com"],
+ [" ACME.OKTA.COM ", "acme.okta.com"],
+ ["Agency.Okta-Gov.com", "agency.okta-gov.com"],
+ ])("normalizes okta org domain %s to %s", (input, expected) => {
+ const result = addProviderFormSchema.safeParse({
+ providerType: "okta",
+ providerUid: input,
+ providerAlias: "okta-test",
+ });
+
+ expect(result.success).toBe(true);
+ expect(
+ result.success && "providerUid" in result.data
+ ? result.data.providerUid
+ : undefined,
+ ).toBe(expected);
+ });
+
+ const invalidUidFixtures = [
+ "https://acme.okta.com",
+ "acme.example.com",
+ "acme.okta.com/path",
+ "",
+ ];
+
+ it.each(invalidUidFixtures)("rejects invalid okta org domain %s", (uid) => {
+ const result = addProviderFormSchema.safeParse({
+ providerType: "okta",
+ providerUid: uid,
+ providerAlias: "okta-test",
+ });
+
+ expect(result.success).toBe(false);
+ });
+});
+
+describe("addCredentialsFormSchema - okta", () => {
+ const BASE_OKTA_VALUES = {
+ [ProviderCredentialFields.PROVIDER_ID]: "provider-okta-1",
+ [ProviderCredentialFields.PROVIDER_TYPE]: "okta",
+ } as const;
+
+ it("accepts okta credentials when client id and private key are present", () => {
+ const schema = addCredentialsFormSchema("okta");
+
+ const result = schema.safeParse({
+ ...BASE_OKTA_VALUES,
+ [ProviderCredentialFields.OKTA_CLIENT_ID]: "0oa123456789abcdef",
+ [ProviderCredentialFields.OKTA_PRIVATE_KEY]:
+ "-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----",
+ });
+
+ expect(result.success).toBe(true);
+ });
+
+ it("reports missing okta private key on okta_private_key field", () => {
+ const schema = addCredentialsFormSchema("okta");
+
+ const result = schema.safeParse({
+ ...BASE_OKTA_VALUES,
+ [ProviderCredentialFields.OKTA_CLIENT_ID]: "0oa123456789abcdef",
+ [ProviderCredentialFields.OKTA_PRIVATE_KEY]: "",
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+
+ expect(result.error.issues).toContainEqual(
+ expect.objectContaining({
+ path: [ProviderCredentialFields.OKTA_PRIVATE_KEY],
+ }),
+ );
+ });
+});
diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts
index a19340b8f0..8c526e1e56 100644
--- a/ui/types/formSchemas.ts
+++ b/ui/types/formSchemas.ts
@@ -163,6 +163,18 @@ export const addProviderFormSchema = z
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string().trim().min(1, "Team ID is required"),
}),
+ z.object({
+ providerType: z.literal("okta"),
+ [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
+ providerUid: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .regex(
+ /^[a-z0-9][a-z0-9-]*\.(okta\.com|oktapreview\.com|okta-emea\.com|okta-gov\.com|okta\.mil|okta-miltest\.com|trex-govcloud\.com)$/,
+ "Org Domain must be an Okta-managed domain (e.g. acme.okta.com), without scheme or path",
+ ),
+ }),
]),
);
@@ -391,7 +403,23 @@ export const addCredentialsFormSchema = (
.trim()
.min(1, "API Token is required"),
}
- : {}),
+ : providerType === "okta"
+ ? {
+ [ProviderCredentialFields.OKTA_CLIENT_ID]:
+ z
+ .string()
+ .trim()
+ .min(1, "Client ID is required"),
+ [ProviderCredentialFields.OKTA_PRIVATE_KEY]:
+ z
+ .string()
+ .trim()
+ .min(
+ 1,
+ "Private Key is required",
+ ),
+ }
+ : {}),
})
.superRefine((data: Record, ctx) => {
if (providerType === "m365") {
diff --git a/ui/types/providers.ts b/ui/types/providers.ts
index 2669366536..1a49bb219f 100644
--- a/ui/types/providers.ts
+++ b/ui/types/providers.ts
@@ -14,6 +14,7 @@ export const PROVIDER_TYPES = [
"cloudflare",
"openstack",
"vercel",
+ "okta",
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];
@@ -34,6 +35,7 @@ export const PROVIDER_DISPLAY_NAMES: Record = {
cloudflare: "Cloudflare",
openstack: "OpenStack",
vercel: "Vercel",
+ okta: "Okta",
};
export function getProviderDisplayName(providerId: string): string {