diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 5d4ccc021..5f8dea5d7 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -6,6 +6,7 @@ import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-r import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router"; +import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router"; import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; @@ -22,5 +23,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter, [SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter, [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter, - [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter + [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter, + [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/okta-client-secret-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/okta-client-secret-rotation-router.ts new file mode 100644 index 000000000..133a70457 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/okta-client-secret-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreateOktaClientSecretRotationSchema, + OktaClientSecretRotationGeneratedCredentialsSchema, + OktaClientSecretRotationSchema, + UpdateOktaClientSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/okta-client-secret"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerOktaClientSecretRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.OktaClientSecret, + server, + responseSchema: OktaClientSecretRotationSchema, + createSchema: CreateOktaClientSecretRotationSchema, + updateSchema: UpdateOktaClientSecretRotationSchema, + generatedCredentialsSchema: OktaClientSecretRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index 86768c3ad..7db99c8c4 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -7,6 +7,7 @@ import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; +import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret"; import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; @@ -23,7 +24,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ Auth0ClientSecretRotationListItemSchema, AzureClientSecretRotationListItemSchema, AwsIamUserSecretRotationListItemSchema, - LdapPasswordRotationListItemSchema + LdapPasswordRotationListItemSchema, + OktaClientSecretRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/index.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/index.ts new file mode 100644 index 000000000..8a1026194 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/index.ts @@ -0,0 +1,3 @@ +export * from "./okta-client-secret-rotation-constants"; +export * from "./okta-client-secret-rotation-schemas"; +export * from "./okta-client-secret-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-constants.ts new file mode 100644 index 000000000..35347f6b4 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "Okta Client Secret", + type: SecretRotation.OktaClientSecret, + connection: AppConnection.Okta, + template: { + secretsMapping: { + clientId: "OKTA_CLIENT_ID", + clientSecret: "OKTA_CLIENT_SECRET" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-fns.ts new file mode 100644 index 000000000..0fe4438d1 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-fns.ts @@ -0,0 +1,273 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { request } from "@app/lib/config/request"; +import { delay as delayMs } from "@app/lib/delay"; +import { BadRequestError } from "@app/lib/errors"; +import { getOktaInstanceUrl } from "@app/services/app-connection/okta"; + +import { + TOktaClientSecret, + TOktaClientSecretRotationGeneratedCredentials, + TOktaClientSecretRotationWithConnection +} from "./okta-client-secret-rotation-types"; + +type OktaErrorResponse = { errorCode: string; errorSummary: string; errorCauses?: { errorSummary: string }[] }; + +const isOktaErrorResponse = (data: unknown): data is OktaErrorResponse => { + return ( + typeof data === "object" && + data !== null && + "errorSummary" in data && + typeof (data as OktaErrorResponse).errorSummary === "string" + ); +}; + +const createErrorMessage = (error: unknown) => { + if (error instanceof AxiosError) { + if (error.response?.data && isOktaErrorResponse(error.response.data)) { + const oktaError = error.response.data; + if (oktaError.errorCauses && oktaError.errorCauses.length > 0) { + return oktaError.errorCauses[0].errorSummary; + } + return oktaError.errorSummary; + } + if (error.message) { + return error.message; + } + } + return "Unknown error"; +}; + +// Delay between each revocation call in revokeCredentials +const DELAY_MS = 1000; + +export const oktaClientSecretRotationFactory: TRotationFactory< + TOktaClientSecretRotationWithConnection, + TOktaClientSecretRotationGeneratedCredentials +> = (secretRotation) => { + const { + connection, + parameters: { clientId }, + secretsMapping + } = secretRotation; + + /** + * Creates a new client secret for the Okta app. + */ + const $rotateClientSecret = async () => { + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + const { data } = await request.post( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets`, + {}, + { + headers: { + Accept: "application/json", + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + if (!data.client_secret || !data.id) { + throw new Error("Invalid response from Okta: missing 'client_secret' or secret 'id'."); + } + + return { + clientSecret: data.client_secret, + secretId: data.id, + clientId + }; + } catch (error: unknown) { + if ( + error instanceof AxiosError && + error.response?.data && + isOktaErrorResponse(error.response.data) && + error.response.data.errorCode === "E0000001" + ) { + // Okta has a maximum of 2 secrets per app, thus we must warn the users in case they already have 2 + throw new BadRequestError({ + message: `Failed to add client secret to Okta app ${clientId}: You must have only a single secret for the Okta app prior to creating this secret rotation.` + }); + } + + throw new BadRequestError({ + message: `Failed to add client secret to Okta app ${clientId}: ${createErrorMessage(error)}` + }); + } + }; + + /** + * List client secrets. + */ + const $listClientSecrets = async () => { + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + const { data } = await request.get( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets`, + { + headers: { + Accept: "application/json", + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + return data; + } catch (error: unknown) { + throw new BadRequestError({ + message: `Failed to list client secrets for Okta app ${clientId}: ${createErrorMessage(error)}` + }); + } + }; + + /** + * Checks if a credential with the given secretId exists. + */ + const credentialExists = async (secretId: string): Promise => { + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + const { data } = await request.get( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}`, + { + headers: { + Accept: "application/json", + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + return data.id === secretId; + } catch (_) { + return false; + } + }; + + /** + * Revokes a client secret from the Okta app using its secretId. + * First checks if the credential exists before attempting revocation. + */ + const revokeCredential = async (secretId: string) => { + // Check if credential exists before attempting revocation + const exists = await credentialExists(secretId); + if (!exists) { + return; // Credential doesn't exist, nothing to revoke + } + + const instanceUrl = await getOktaInstanceUrl(connection); + + try { + // First deactivate the secret + await request.post( + `${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}/lifecycle/deactivate`, + undefined, + { + headers: { + Authorization: `SSWS ${connection.credentials.apiToken}` + } + } + ); + + // Then delete it + await request.delete(`${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}`, { + headers: { + Authorization: `SSWS ${connection.credentials.apiToken}` + } + }); + } catch (error: unknown) { + if ( + error instanceof AxiosError && + error.response?.data && + isOktaErrorResponse(error.response.data) && + error.response.data.errorCode === "E0000001" + ) { + // If this is the last secret, we cannot revoke it + return; + } + + throw new BadRequestError({ + message: `Failed to remove client secret with secretId ${secretId} from app ${clientId}: ${createErrorMessage(error)}` + }); + } + }; + + /** + * Issues a new set of credentials. + */ + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotateClientSecret(); + return callback(credentials); + }; + + /** + * Revokes a list of credentials. + */ + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + credentials, + callback + ) => { + if (!credentials?.length) return callback(); + + for (const { secretId } of credentials) { + await revokeCredential(secretId); + await delayMs(DELAY_MS); + } + return callback(); + }; + + /** + * Rotates credentials by issuing new ones and revoking the old. + */ + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + oldCredentials, + callback, + activeCredentials + ) => { + // Since in Okta you can only have a maximum of 2 secrets at a time, we must delete any other secret besides the current one PRIOR to generating the second secret + if (oldCredentials?.secretId) { + await revokeCredential(oldCredentials.secretId); + } else if (activeCredentials) { + // On the first rotation oldCredentials won't be set so we must find the second secret manually + const secrets = await $listClientSecrets(); + + if (secrets.length > 1) { + const nonActiveSecret = secrets.find((secret) => secret.id !== activeCredentials.secretId); + if (nonActiveSecret) { + await revokeCredential(nonActiveSecret.id); + } + } + } + + const newCredentials = await $rotateClientSecret(); + return callback(newCredentials); + }; + + /** + * Maps the generated credentials into the secret payload format. + */ + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ({ + clientSecret + }) => [ + { key: secretsMapping.clientId, value: clientId }, + { key: secretsMapping.clientSecret, value: clientSecret } + ]; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-schemas.ts new file mode 100644 index 000000000..9325d9518 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-schemas.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const OktaClientSecretRotationGeneratedCredentialsSchema = z + .object({ + clientId: z.string(), + clientSecret: z.string(), + secretId: z.string() + }) + .array() + .min(1) + .max(2); + +const OktaClientSecretRotationParametersSchema = z.object({ + clientId: z + .string() + .trim() + .min(1, "Client ID Required") + .describe(SecretRotations.PARAMETERS.OKTA_CLIENT_SECRET.clientId) +}); + +const OktaClientSecretRotationSecretsMappingSchema = z.object({ + clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.OKTA_CLIENT_SECRET.clientId), + clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.OKTA_CLIENT_SECRET.clientSecret) +}); + +export const OktaClientSecretRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + clientId: z.string(), + clientSecret: z.string() + }) +}); + +export const OktaClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.OktaClientSecret).extend({ + type: z.literal(SecretRotation.OktaClientSecret), + parameters: OktaClientSecretRotationParametersSchema, + secretsMapping: OktaClientSecretRotationSecretsMappingSchema +}); + +export const CreateOktaClientSecretRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.OktaClientSecret +).extend({ + parameters: OktaClientSecretRotationParametersSchema, + secretsMapping: OktaClientSecretRotationSecretsMappingSchema +}); + +export const UpdateOktaClientSecretRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.OktaClientSecret +).extend({ + parameters: OktaClientSecretRotationParametersSchema.optional(), + secretsMapping: OktaClientSecretRotationSecretsMappingSchema.optional() +}); + +export const OktaClientSecretRotationListItemSchema = z.object({ + name: z.literal("Okta Client Secret"), + connection: z.literal(AppConnection.Okta), + type: z.literal(SecretRotation.OktaClientSecret), + template: OktaClientSecretRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-types.ts new file mode 100644 index 000000000..101b4839e --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/okta-client-secret/okta-client-secret-rotation-types.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { TOktaConnection } from "@app/services/app-connection/okta"; + +import { + CreateOktaClientSecretRotationSchema, + OktaClientSecretRotationGeneratedCredentialsSchema, + OktaClientSecretRotationListItemSchema, + OktaClientSecretRotationSchema +} from "./okta-client-secret-rotation-schemas"; + +export type TOktaClientSecretRotation = z.infer; + +export type TOktaClientSecretRotationInput = z.infer; + +export type TOktaClientSecretRotationListItem = z.infer; + +export type TOktaClientSecretRotationWithConnection = TOktaClientSecretRotation & { + connection: TOktaConnection; +}; + +export type TOktaClientSecretRotationGeneratedCredentials = z.infer< + typeof OktaClientSecretRotationGeneratedCredentialsSchema +>; + +export interface TOktaClientSecretRotationParameters { + clientId: string; + secretId: string; +} + +export interface TOktaClientSecretRotationSecretsMapping { + clientId: string; + clientSecret: string; + secretId: string; +} + +export interface TOktaClientSecret { + id: string; + client_secret: string; +} diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index 84dc30821..cf0fe578a 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -6,7 +6,8 @@ export enum SecretRotation { Auth0ClientSecret = "auth0-client-secret", AzureClientSecret = "azure-client-secret", AwsIamUserSecret = "aws-iam-user-secret", - LdapPassword = "ldap-password" + LdapPassword = "ldap-password", + OktaClientSecret = "okta-client-secret" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 228c4c2a1..7c0239add 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -10,6 +10,7 @@ import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret" import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials"; +import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret"; import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; @@ -30,7 +31,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 029c9bdc5..d9a771101 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -9,7 +9,8 @@ export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", [SecretRotation.AzureClientSecret]: "Azure Client Secret", [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret", - [SecretRotation.LdapPassword]: "LDAP Password" + [SecretRotation.LdapPassword]: "LDAP Password", + [SecretRotation.OktaClientSecret]: "Okta Client Secret" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { @@ -20,5 +21,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record { diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 287a406f6..3bbdc363d 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -25,6 +25,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; +import { registerOktaConnectionRouter } from "./okta-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerRailwayConnectionRouter } from "./railway-connection-router"; import { registerRenderConnectionRouter } from "./render-connection-router"; @@ -72,5 +73,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.Okta, + server, + sanitizedResponseSchema: SanitizedOktaConnectionSchema, + createSchema: CreateOktaConnectionSchema, + updateSchema: UpdateOktaConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/apps`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + apps: z.object({ id: z.string(), label: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId } + } = req; + + const apps = await server.services.appConnection.okta.listApps(connectionId, req.permission); + return { apps }; + } + }); +}; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 233ce0ea8..7fcdc7217 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -32,7 +32,8 @@ export enum AppConnection { Railway = "railway", Bitbucket = "bitbucket", Checkly = "checkly", - Supabase = "supabase" + Supabase = "supabase", + Okta = "okta" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 54aac2001..9568761f7 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -92,6 +92,7 @@ import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnection import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; +import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; import { RenderConnectionMethod } from "./render/render-connection-enums"; @@ -155,7 +156,8 @@ export const listAppConnectionOptions = () => { getRailwayConnectionListItem(), getBitbucketConnectionListItem(), getChecklyConnectionListItem(), - getSupabaseConnectionListItem() + getSupabaseConnectionListItem(), + getOktaConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -241,7 +243,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService); @@ -280,6 +283,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case CloudflareConnectionMethod.APIToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case OktaConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -367,7 +371,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Checkly]: platformManagedCredentialsNotSupported, - [AppConnection.Supabase]: platformManagedCredentialsNotSupported + [AppConnection.Supabase]: platformManagedCredentialsNotSupported, + [AppConnection.Okta]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 8a85020d8..03b979312 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -34,7 +34,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Railway]: "Railway", [AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Checkly]: "Checkly", - [AppConnection.Supabase]: "Supabase" + [AppConnection.Supabase]: "Supabase", + [AppConnection.Okta]: "Okta" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -71,5 +72,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -273,6 +280,7 @@ export type TAppConnectionInput = { id: string } & ( | TRailwayConnectionInput | TChecklyConnectionInput | TSupabaseConnectionInput + | TOktaConnectionInput ); export type TSqlConnectionInput = @@ -321,7 +329,8 @@ export type TAppConnectionConfig = | TZabbixConnectionConfig | TRailwayConnectionConfig | TChecklyConnectionConfig - | TSupabaseConnectionConfig; + | TSupabaseConnectionConfig + | TOktaConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -357,7 +366,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateZabbixConnectionCredentialsSchema | TValidateRailwayConnectionCredentialsSchema | TValidateChecklyConnectionCredentialsSchema - | TValidateSupabaseConnectionCredentialsSchema; + | TValidateSupabaseConnectionCredentialsSchema + | TValidateOktaConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/okta/index.ts b/backend/src/services/app-connection/okta/index.ts new file mode 100644 index 000000000..ce06fe7e2 --- /dev/null +++ b/backend/src/services/app-connection/okta/index.ts @@ -0,0 +1,4 @@ +export * from "./okta-connection-enums"; +export * from "./okta-connection-fns"; +export * from "./okta-connection-schemas"; +export * from "./okta-connection-types"; diff --git a/backend/src/services/app-connection/okta/okta-connection-enums.ts b/backend/src/services/app-connection/okta/okta-connection-enums.ts new file mode 100644 index 000000000..75bd5ea61 --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-enums.ts @@ -0,0 +1,3 @@ +export enum OktaConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/okta/okta-connection-fns.ts b/backend/src/services/app-connection/okta/okta-connection-fns.ts new file mode 100644 index 000000000..a48eebbb8 --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-fns.ts @@ -0,0 +1,57 @@ +import { request } from "@app/lib/config/request"; +import { UnauthorizedError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { OktaConnectionMethod } from "./okta-connection-enums"; +import { TOktaApp, TOktaConnection, TOktaConnectionConfig } from "./okta-connection-types"; + +export const getOktaConnectionListItem = () => { + return { + name: "Okta" as const, + app: AppConnection.Okta as const, + methods: Object.values(OktaConnectionMethod) as [OktaConnectionMethod.ApiToken] + }; +}; + +export const getOktaInstanceUrl = async (config: TOktaConnectionConfig) => { + const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + await blockLocalAndPrivateIpAddresses(instanceUrl); + return instanceUrl; +}; + +export const validateOktaConnectionCredentials = async (config: TOktaConnectionConfig) => { + const { apiToken } = config.credentials; + const instanceUrl = await getOktaInstanceUrl(config); + + try { + await request.get(`${instanceUrl}/api/v1/users/me`, { + headers: { + Accept: "application/json", + Authorization: `SSWS ${apiToken}` + }, + validateStatus: (status) => status === 200 + }); + } catch (error: unknown) { + throw new UnauthorizedError({ + message: "Unable to validate connection: invalid credentials" + }); + } + + return config.credentials; +}; + +export const listOktaApps = async (appConnection: TOktaConnection) => { + const { apiToken } = appConnection.credentials; + const instanceUrl = await getOktaInstanceUrl(appConnection); + + const { data } = await request.get(`${instanceUrl}/api/v1/apps`, { + headers: { + Accept: "application/json", + Authorization: `SSWS ${apiToken}` + } + }); + + return data.filter((app) => app.status === "ACTIVE" && app.name === "oidc_client"); +}; diff --git a/backend/src/services/app-connection/okta/okta-connection-schemas.ts b/backend/src/services/app-connection/okta/okta-connection-schemas.ts new file mode 100644 index 000000000..37ce0ec11 --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-schemas.ts @@ -0,0 +1,69 @@ +import RE2 from "re2"; +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { OktaConnectionMethod } from "./okta-connection-enums"; + +export const OktaConnectionApiTokenCredentialsSchema = z.object({ + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .min(1, "Instance URL required") + .max(255) + .describe(AppConnections.CREDENTIALS.OKTA.instanceUrl), + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .refine((value) => new RE2("^00[a-zA-Z0-9_-]{40}$").test(value), "Invalid Okta API Token format") + .describe(AppConnections.CREDENTIALS.OKTA.apiToken) +}); + +const BaseOktaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Okta) }); + +export const OktaConnectionSchema = BaseOktaConnectionSchema.extend({ + method: z.literal(OktaConnectionMethod.ApiToken), + credentials: OktaConnectionApiTokenCredentialsSchema +}); + +export const SanitizedOktaConnectionSchema = z.discriminatedUnion("method", [ + BaseOktaConnectionSchema.extend({ + method: z.literal(OktaConnectionMethod.ApiToken), + credentials: OktaConnectionApiTokenCredentialsSchema.pick({ + instanceUrl: true + }) + }) +]); + +export const ValidateOktaConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(OktaConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Okta).method), + credentials: OktaConnectionApiTokenCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Okta).credentials) + }) +]); + +export const CreateOktaConnectionSchema = ValidateOktaConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Okta) +); + +export const UpdateOktaConnectionSchema = z + .object({ + credentials: OktaConnectionApiTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Okta).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Okta)); + +export const OktaConnectionListItemSchema = z.object({ + name: z.literal("Okta"), + app: z.literal(AppConnection.Okta), + methods: z.nativeEnum(OktaConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/okta/okta-connection-service.ts b/backend/src/services/app-connection/okta/okta-connection-service.ts new file mode 100644 index 000000000..8ac036dcd --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-service.ts @@ -0,0 +1,23 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listOktaApps } from "./okta-connection-fns"; +import { TOktaConnection } from "./okta-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const oktaConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listApps = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Okta, connectionId, actor); + const apps = await listOktaApps(appConnection); + return apps; + }; + + return { + listApps + }; +}; diff --git a/backend/src/services/app-connection/okta/okta-connection-types.ts b/backend/src/services/app-connection/okta/okta-connection-types.ts new file mode 100644 index 000000000..8ed53aaed --- /dev/null +++ b/backend/src/services/app-connection/okta/okta-connection-types.ts @@ -0,0 +1,29 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateOktaConnectionSchema, + OktaConnectionSchema, + ValidateOktaConnectionCredentialsSchema +} from "./okta-connection-schemas"; + +export type TOktaConnection = z.infer; + +export type TOktaConnectionInput = z.infer & { + app: AppConnection.Okta; +}; + +export type TValidateOktaConnectionCredentialsSchema = typeof ValidateOktaConnectionCredentialsSchema; + +export type TOktaConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TOktaApp = { + id: string; + label: string; + status: "ACTIVE" | "INACTIVE"; + name: "oidc_client"; // "oidc_client" or other types +}; diff --git a/docs/api-reference/endpoints/app-connections/okta/available.mdx b/docs/api-reference/endpoints/app-connections/okta/available.mdx new file mode 100644 index 000000000..169ddb51b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/okta/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/create.mdx b/docs/api-reference/endpoints/app-connections/okta/create.mdx new file mode 100644 index 000000000..732f83fa8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/okta" +--- + + + Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/okta/delete.mdx b/docs/api-reference/endpoints/app-connections/okta/delete.mdx new file mode 100644 index 000000000..09abf8549 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/okta/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/okta/get-by-id.mdx new file mode 100644 index 000000000..789f7b87d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/okta/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/okta/get-by-name.mdx new file mode 100644 index 000000000..763d42d72 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/okta/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/list.mdx b/docs/api-reference/endpoints/app-connections/okta/list.mdx new file mode 100644 index 000000000..81ac560f2 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/okta" +--- diff --git a/docs/api-reference/endpoints/app-connections/okta/update.mdx b/docs/api-reference/endpoints/app-connections/okta/update.mdx new file mode 100644 index 000000000..b063eeade --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/okta/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/okta/{connectionId}" +--- + + + Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/create.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/create.mdx new file mode 100644 index 000000000..a92c1bc10 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/okta-client-secret" +--- + + + Check out the configuration docs for [Okta Client Secret Rotations](/documentation/platform/secret-rotation/okta-client-secret) to learn how to obtain the required parameters. + diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/delete.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/delete.mdx new file mode 100644 index 000000000..598eb1559 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/okta-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id.mdx new file mode 100644 index 000000000..b2c9c281e --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name.mdx new file mode 100644 index 000000000..0eb400b7d --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/okta-client-secret/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..a74c52d92 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/list.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/list.mdx new file mode 100644 index 000000000..bb8b6777f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/okta-client-secret" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets.mdx new file mode 100644 index 000000000..71f7f2fbf --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/okta-client-secret/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/okta-client-secret/update.mdx b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/update.mdx new file mode 100644 index 000000000..3cae3f895 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/okta-client-secret/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/okta-client-secret/{rotationId}" +--- + + + Check out the configuration docs for [Okta Client Secret Rotations](/documentation/platform/secret-rotation/okta-client-secret) to learn how to obtain the required parameters. + diff --git a/docs/docs.json b/docs/docs.json index a32453c89..48182dcf9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -78,7 +78,10 @@ }, { "group": "Infisical SSH", - "pages": ["documentation/platform/ssh/overview", "documentation/platform/ssh/host-groups"] + "pages": [ + "documentation/platform/ssh/overview", + "documentation/platform/ssh/host-groups" + ] }, { "group": "Key Management (KMS)", @@ -146,6 +149,7 @@ "documentation/platform/secret-rotation/ldap-password", "documentation/platform/secret-rotation/mssql-credentials", "documentation/platform/secret-rotation/mysql-credentials", + "documentation/platform/secret-rotation/okta-client-secret", "documentation/platform/secret-rotation/oracledb-credentials", "documentation/platform/secret-rotation/postgres-credentials" ] @@ -375,7 +379,10 @@ }, { "group": "Architecture", - "pages": ["internals/architecture/components", "internals/architecture/cloud"] + "pages": [ + "internals/architecture/components", + "internals/architecture/cloud" + ] }, "internals/security", "internals/service-tokens" @@ -481,6 +488,7 @@ "integrations/app-connections/mssql", "integrations/app-connections/mysql", "integrations/app-connections/oci", + "integrations/app-connections/okta", "integrations/app-connections/oracledb", "integrations/app-connections/postgres", "integrations/app-connections/railway", @@ -551,7 +559,10 @@ "integrations/cloud/gcp-secret-manager", { "group": "Cloudflare", - "pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"] + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] }, "integrations/cloud/terraform-cloud", "integrations/cloud/databricks", @@ -663,7 +674,11 @@ "cli/commands/reset", { "group": "infisical scan", - "pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"] + "pages": [ + "cli/commands/scan", + "cli/commands/scan-git-changes", + "cli/commands/scan-install" + ] } ] }, @@ -987,7 +1002,9 @@ "pages": [ { "group": "Kubernetes", - "pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"] + "pages": [ + "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" + ] }, "api-reference/endpoints/dynamic-secrets/create", "api-reference/endpoints/dynamic-secrets/update", @@ -1093,6 +1110,19 @@ "api-reference/endpoints/secret-rotations/mysql-credentials/update" ] }, + { + "group": "Okta Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/okta-client-secret/create", + "api-reference/endpoints/secret-rotations/okta-client-secret/delete", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/okta-client-secret/list", + "api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/okta-client-secret/update" + ] + }, { "group": "OracleDB Credentials", "pages": [ @@ -1496,6 +1526,18 @@ "api-reference/endpoints/app-connections/oci/delete" ] }, + { + "group": "Okta", + "pages": [ + "api-reference/endpoints/app-connections/okta/list", + "api-reference/endpoints/app-connections/okta/available", + "api-reference/endpoints/app-connections/okta/get-by-id", + "api-reference/endpoints/app-connections/okta/get-by-name", + "api-reference/endpoints/app-connections/okta/create", + "api-reference/endpoints/app-connections/okta/update", + "api-reference/endpoints/app-connections/okta/delete" + ] + }, { "group": "OracleDB", "pages": [ diff --git a/docs/documentation/platform/secret-rotation/okta-client-secret.mdx b/docs/documentation/platform/secret-rotation/okta-client-secret.mdx new file mode 100644 index 000000000..d1f4b159e --- /dev/null +++ b/docs/documentation/platform/secret-rotation/okta-client-secret.mdx @@ -0,0 +1,145 @@ +--- +title: "Okta Client Secret" +description: "Learn how to automatically rotate Okta Client Secrets." +--- + +## Prerequisites + +- Create an [Okta Connection](/integrations/app-connections/okta). + +## Create an Okta Client Secret Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **Okta Client Secret** option. + + ![Select Okta Client Secret](/images/secret-rotations-v2/okta-client-secret/select-okta.png) + + 3. Configure the rotation behavior, then click **Next**. + + ![Rotation Configuration](/images/secret-rotations-v2/okta-client-secret/configuration.png) + + - **Okta Connection** - the connection that will perform the rotation of the specified application's Client Secret. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + 4. Select the Okta application whose Client Secret you want to rotate. Then click **Next**. + + ![Rotation Parameters](/images/secret-rotations-v2/okta-client-secret/parameters.png) + + 5. Specify the secret names that the client credentials should be mapped to. Then click **Next**. + + ![Rotation Secrets Mapping](/images/secret-rotations-v2/okta-client-secret/mappings.png) + + - **Client ID** - the name of the secret that the application Client ID will be mapped to. + - **Client Secret** - the name of the secret that the rotated Client Secret will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + + ![Rotation Details](/images/secret-rotations-v2/okta-client-secret/details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + + ![Rotation Review](/images/secret-rotations-v2/okta-client-secret/review.png) + + 8. Your **Okta Client Secret** credentials are now available for use via the mapped secrets. + + ![Rotation Created](/images/secret-rotations-v2/okta-client-secret/created.png) + + + To create an Okta Client Secret Rotation, make an API request to the [Create Okta Client Secret Rotation](/api-reference/endpoints/secret-rotations/okta-client-secret/create) API endpoint. + + You will first need the **Client ID** of the Okta application you want to rotate the secret for. This can be obtained from the applications dashboard. + + ![Okta Client ID](/images/secret-rotations-v2/okta-client-secret/client-id.png) + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/okta-client-secret \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-okta-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my client secret rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "clientId": "...", + }, + "secretsMapping": { + "clientId": "OKTA_CLIENT_ID", + "clientSecret": "OKTA_CLIENT_SECRET" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-okta-rotation", + "description": "my client secret rotation", + "secretsMapping": { + "clientId": "OKTA_CLIENT_ID", + "clientSecret": "OKTA_CLIENT_SECRET" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "okta", + "name": "my-okta-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "okta-client-secret", + "parameters": { + "clientId": "..." + } + } + } + ``` + + diff --git a/docs/images/app-connections/okta/step-1.png b/docs/images/app-connections/okta/step-1.png new file mode 100644 index 000000000..478e87705 Binary files /dev/null and b/docs/images/app-connections/okta/step-1.png differ diff --git a/docs/images/app-connections/okta/step-2.png b/docs/images/app-connections/okta/step-2.png new file mode 100644 index 000000000..69645bf84 Binary files /dev/null and b/docs/images/app-connections/okta/step-2.png differ diff --git a/docs/images/app-connections/okta/step-3.png b/docs/images/app-connections/okta/step-3.png new file mode 100644 index 000000000..de8d69a24 Binary files /dev/null and b/docs/images/app-connections/okta/step-3.png differ diff --git a/docs/images/app-connections/okta/step-4.png b/docs/images/app-connections/okta/step-4.png new file mode 100644 index 000000000..eb6426a37 Binary files /dev/null and b/docs/images/app-connections/okta/step-4.png differ diff --git a/docs/images/app-connections/okta/step-5.png b/docs/images/app-connections/okta/step-5.png new file mode 100644 index 000000000..54bf24bde Binary files /dev/null and b/docs/images/app-connections/okta/step-5.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/client-id.png b/docs/images/secret-rotations-v2/okta-client-secret/client-id.png new file mode 100644 index 000000000..83ac00c4f Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/client-id.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/configuration.png b/docs/images/secret-rotations-v2/okta-client-secret/configuration.png new file mode 100644 index 000000000..fe511bbcc Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/configuration.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/created.png b/docs/images/secret-rotations-v2/okta-client-secret/created.png new file mode 100644 index 000000000..4e0c8a5da Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/created.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/details.png b/docs/images/secret-rotations-v2/okta-client-secret/details.png new file mode 100644 index 000000000..6eafb89bf Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/details.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/mappings.png b/docs/images/secret-rotations-v2/okta-client-secret/mappings.png new file mode 100644 index 000000000..baeaa1605 Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/mappings.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/parameters.png b/docs/images/secret-rotations-v2/okta-client-secret/parameters.png new file mode 100644 index 000000000..b5a6a716b Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/parameters.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/review.png b/docs/images/secret-rotations-v2/okta-client-secret/review.png new file mode 100644 index 000000000..45462ceb5 Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/review.png differ diff --git a/docs/images/secret-rotations-v2/okta-client-secret/select-okta.png b/docs/images/secret-rotations-v2/okta-client-secret/select-okta.png new file mode 100644 index 000000000..34347245f Binary files /dev/null and b/docs/images/secret-rotations-v2/okta-client-secret/select-okta.png differ diff --git a/docs/integrations/app-connections/okta.mdx b/docs/integrations/app-connections/okta.mdx new file mode 100644 index 000000000..3c1295cf8 --- /dev/null +++ b/docs/integrations/app-connections/okta.mdx @@ -0,0 +1,99 @@ +--- +title: "Okta Connection" +description: "Learn how to configure an Okta Connection for Infisical." +--- + +Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guides/create-an-api-token/main/) to connect with Okta. + +## Create Okta API Token + + + + From the Okta admin dashboard, navigate to **Security > API > Tokens** and click **Create token**. + + ![Create API Token](/images/app-connections/okta/step-1.png) + + + Enter the token name and select **Any IP** for the second dropdown, then click **Create token**. + + ![Provide Info](/images/app-connections/okta/step-2.png) + + + Copy the token from the modal for later steps. + + ![Copy Token](/images/app-connections/okta/step-3.png) + + + +## Create Okta Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **Add Connection** button and select **Okta** from the list of available connections. + + + Complete the Okta Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - Your Okta instance URL + - The API Token from earlier steps + + ![Connection Modal](/images/app-connections/okta/step-4.png) + + + After clicking Create, your **Okta Connection** is established and ready to use with your Infisical projects. + + ![Connection Created](/images/app-connections/okta/step-5.png) + + + + + To create a Okta Connection, make an API request to the [Create Okta Connection](/api-reference/endpoints/app-connections/okta/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/okta \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-okta-connection", + "method": "api-token", + "credentials": { + "instanceUrl": "https://example.okta.com", + "apiToken": "" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-okta-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "okta", + "method": "api-token", + "credentials": { + "instanceUrl": "https://example.okta.com" + } + } + } + ``` + + diff --git a/frontend/public/images/integrations/Okta.png b/frontend/public/images/integrations/Okta.png new file mode 100644 index 000000000..d742d4347 Binary files /dev/null and b/frontend/public/images/integrations/Okta.png differ diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewOktaClientSecretRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewOktaClientSecretRotationGeneratedCredentials.tsx new file mode 100644 index 000000000..d109ae0db --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewOktaClientSecretRotationGeneratedCredentials.tsx @@ -0,0 +1,38 @@ +import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay"; +import { TOktaClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/okta-client-secret-rotation"; + +import { ViewRotationGeneratedCredentialsDisplay } from "./shared"; + +type Props = { + generatedCredentialsResponse: TOktaClientSecretRotationGeneratedCredentialsResponse; +}; + +export const ViewOktaClientSecretRotationGeneratedCredentials = ({ + generatedCredentialsResponse: { generatedCredentials, activeIndex } +}: Props) => { + const inactiveIndex = activeIndex === 0 ? 1 : 0; + + const activeCredentials = generatedCredentials[activeIndex]; + const inactiveCredentials = generatedCredentials[inactiveIndex]; + + return ( + + {activeCredentials?.clientId} + + {activeCredentials?.clientSecret} + + + } + inactiveCredentials={ + <> + {inactiveCredentials?.clientId} + + {inactiveCredentials?.clientSecret} + + + } + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx index c81eb9920..33d3fccc1 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx @@ -22,6 +22,7 @@ import { import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared"; import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials"; +import { ViewOktaClientSecretRotationGeneratedCredentials } from "./ViewOktaClientSecretRotationGeneratedCredentials"; type Props = { secretRotation?: TSecretRotationV2; @@ -99,6 +100,13 @@ const Content = ({ secretRotation }: ContentProps) => { /> ); break; + case SecretRotation.OktaClientSecret: + Component = ( + + ); + break; default: throw new Error("Unhandled View Generated Credential Rotation Type"); } diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/OktaClientSecretRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/OktaClientSecretRotationParametersFields.tsx new file mode 100644 index 000000000..bb306615d --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/OktaClientSecretRotationParametersFields.tsx @@ -0,0 +1,51 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { useOktaConnectionListApps } from "@app/hooks/api/appConnections/okta"; +import { TOktaApp } from "@app/hooks/api/appConnections/okta/types"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const OktaClientSecretRotationParametersFields = () => { + const { control, watch, setValue } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.OktaClientSecret; + } + >(); + + const connectionId = watch("connection.id"); + + const { data: apps, isPending: isAppsPending } = useOktaConnectionListApps(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + ( + + app.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("parameters.clientId", (option as SingleValue)?.id ?? ""); + }} + options={apps} + placeholder="Select an application..." + getOptionLabel={(option) => option.label} + getOptionValue={(option) => option.id} + /> + + )} + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx index 959ca2d9e..3f489b04e 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx @@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRo import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields"; import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields"; import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields"; +import { OktaClientSecretRotationParametersFields } from "./OktaClientSecretRotationParametersFields"; import { SqlCredentialsRotationParametersFields } from "./shared"; const COMPONENT_MAP: Record = { @@ -17,7 +18,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields, [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields, - [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields, + [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields }; export const SecretRotationV2ParametersFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/OktaClientSecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/OktaClientSecretRotationReviewFields.tsx new file mode 100644 index 000000000..a9fc4068e --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/OktaClientSecretRotationReviewFields.tsx @@ -0,0 +1,29 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +import { SecretRotationReviewSection } from "./shared"; + +export const OktaClientSecretRotationReviewFields = () => { + const { watch } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.OktaClientSecret; + } + >(); + + const [parameters, { clientId, clientSecret }] = watch(["parameters", "secretsMapping"]); + + return ( + <> + + {parameters.clientId} + + + {clientId} + {clientSecret} + + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index 17cc34f27..636cc98cc 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -10,6 +10,7 @@ import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotati import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields"; import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields"; import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields"; +import { OktaClientSecretRotationReviewFields } from "./OktaClientSecretRotationReviewFields"; import { SqlCredentialsRotationReviewFields } from "./shared"; const COMPONENT_MAP: Record = { @@ -20,7 +21,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields, [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields, - [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields, + [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields }; export const SecretRotationV2ReviewFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/OktaClientSecretRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/OktaClientSecretRotationSecretsMappingFields.tsx new file mode 100644 index 000000000..72adc863d --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/OktaClientSecretRotationSecretsMappingFields.tsx @@ -0,0 +1,58 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2"; + +import { SecretsMappingTable } from "./shared"; + +export const OktaClientSecretRotationSecretsMappingFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.OktaClientSecret; + } + >(); + + const { rotationOption } = useSecretRotationV2Option(SecretRotation.OktaClientSecret); + + const items = [ + { + name: "Client ID", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.clientId" + /> + ) + }, + { + name: "Client Secret", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.clientSecret" + /> + ) + } + ]; + + return ; +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index 428a99161..dd0ce9cab 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecr import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields"; import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields"; import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields"; +import { OktaClientSecretRotationSecretsMappingFields } from "./OktaClientSecretRotationSecretsMappingFields"; import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; const COMPONENT_MAP: Record = { @@ -17,7 +18,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields, [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields, - [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields, + [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields }; export const SecretRotationV2SecretsMappingFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index 77151bf1e..a6ebe2f64 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -10,6 +10,7 @@ import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotati import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; +import { OktaClientSecretRotationSchema } from "./okta-client-secret-rotation-schema"; import { OracleDBCredentialsRotationSchema } from "./oracledb-credentials-rotation-schema"; export const SecretRotationV2FormSchema = (isUpdate: boolean) => @@ -23,7 +24,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) => MySqlCredentialsRotationSchema, OracleDBCredentialsRotationSchema, LdapPasswordRotationSchema, - AwsIamUserSecretRotationSchema + AwsIamUserSecretRotationSchema, + OktaClientSecretRotationSchema ]), z.object({ id: z.string().optional() }) ) diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/okta-client-secret-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/okta-client-secret-rotation-schema.ts new file mode 100644 index 000000000..569ee2c6c --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/okta-client-secret-rotation-schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const OktaClientSecretRotationSchema = z + .object({ + type: z.literal(SecretRotation.OktaClientSecret), + parameters: z.object({ + clientId: z.string().trim().min(1, "App ID required") + }), + secretsMapping: z.object({ + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client Secret required") + }) + }) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 996483904..807569c6e 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -17,7 +17,9 @@ import { AzureClientSecretsConnectionMethod, AzureDevOpsConnectionMethod, AzureKeyVaultConnectionMethod, + BitbucketConnectionMethod, CamundaConnectionMethod, + ChecklyConnectionMethod, CloudflareConnectionMethod, DatabricksConnectionMethod, FlyioConnectionMethod, @@ -26,13 +28,19 @@ import { GitHubRadarConnectionMethod, GitLabConnectionMethod, HCVaultConnectionMethod, + HerokuConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, MsSqlConnectionMethod, MySqlConnectionMethod, + OCIConnectionMethod, + OktaConnectionMethod, OnePassConnectionMethod, OracleDBConnectionMethod, PostgresConnectionMethod, + RailwayConnectionMethod, + RenderConnectionMethod, + SupabaseConnectionMethod, TAppConnection, TeamCityConnectionMethod, TerraformCloudConnectionMethod, @@ -40,13 +48,6 @@ import { WindmillConnectionMethod, ZabbixConnectionMethod } from "@app/hooks/api/appConnections/types"; -import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; -import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection"; -import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; -import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; -import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection"; -import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; -import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, @@ -98,7 +99,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Railway]: { name: "Railway", image: "Railway.png" }, [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" }, - [AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" } + [AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" }, + [AppConnection.Okta]: { name: "Okta", image: "Okta.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -132,6 +134,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case CloudflareConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case OktaConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index 6e484881d..2979a7623 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -44,6 +44,11 @@ export const SECRET_ROTATION_MAP: Record< name: "AWS IAM User Secret", image: "Amazon Web Services.png", size: 50 + }, + [SecretRotation.OktaClientSecret]: { + name: "Okta Client Secret", + image: "Okta.png", + size: 50 } }; @@ -55,7 +60,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.Auth0ClientSecret]: false, [SecretRotation.AzureClientSecret]: true, [SecretRotation.LdapPassword]: false, - [SecretRotation.AwsIamUserSecret]: true + [SecretRotation.AwsIamUserSecret]: true, + [SecretRotation.OktaClientSecret]: true }; export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 965675bc7..2e1e59655 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -32,5 +32,6 @@ export enum AppConnection { Zabbix = "zabbix", Railway = "railway", Checkly = "checkly", - Supabase = "supabase" + Supabase = "supabase", + Okta = "okta" } diff --git a/frontend/src/hooks/api/appConnections/okta/index.ts b/frontend/src/hooks/api/appConnections/okta/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/okta/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/okta/queries.tsx b/frontend/src/hooks/api/appConnections/okta/queries.tsx new file mode 100644 index 000000000..9823a296a --- /dev/null +++ b/frontend/src/hooks/api/appConnections/okta/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TOktaApp } from "./types"; + +const oktaConnectionKeys = { + all: [...appConnectionKeys.all, "okta"] as const, + listApps: (connectionId: string) => [...oktaConnectionKeys.all, "apps", connectionId] as const +}; + +export const useOktaConnectionListApps = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TOktaApp[], + unknown, + TOktaApp[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: oktaConnectionKeys.listApps(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ apps: TOktaApp[] }>( + `/api/v1/app-connections/okta/${connectionId}/apps` + ); + + return data.apps; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/okta/types.ts b/frontend/src/hooks/api/appConnections/okta/types.ts new file mode 100644 index 000000000..8acc6c04d --- /dev/null +++ b/frontend/src/hooks/api/appConnections/okta/types.ts @@ -0,0 +1,4 @@ +export type TOktaApp = { + id: string; + label: string; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index b6b00a615..e9bc5b243 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -152,6 +152,10 @@ export type TSupabaseConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Supabase; }; +export type TOktaConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Okta; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -183,7 +187,8 @@ export type TAppConnectionOption = | TBitbucketConnectionOption | TZabbixConnectionOption | TRailwayConnectionOption - | TChecklyConnectionOption; + | TChecklyConnectionOption + | TOktaConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -220,4 +225,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Railway]: TRailwayConnectionOption; [AppConnection.Checkly]: TChecklyConnectionOption; [AppConnection.Supabase]: TSupabaseConnectionOption; + [AppConnection.Okta]: TOktaConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 3fc659793..5085feab3 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -24,6 +24,7 @@ import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TMySqlConnection } from "./mysql-connection"; import { TOCIConnection } from "./oci-connection"; +import { TOktaConnection } from "./okta-connection"; import { TOracleDBConnection } from "./oracledb-connection"; import { TPostgresConnection } from "./postgres-connection"; import { TRailwayConnection } from "./railway-connection"; @@ -44,6 +45,7 @@ export * from "./azure-devops-connection"; export * from "./azure-key-vault-connection"; export * from "./bitbucket-connection"; export * from "./camunda-connection"; +export * from "./checkly-connection"; export * from "./cloudflare-connection"; export * from "./databricks-connection"; export * from "./flyio-connection"; @@ -58,9 +60,12 @@ export * from "./ldap-connection"; export * from "./mssql-connection"; export * from "./mysql-connection"; export * from "./oci-connection"; +export * from "./okta-connection"; export * from "./oracledb-connection"; export * from "./postgres-connection"; +export * from "./railway-connection"; export * from "./render-connection"; +export * from "./supabase-connection"; export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; @@ -101,7 +106,8 @@ export type TAppConnection = | TZabbixConnection | TRailwayConnection | TChecklyConnection - | TSupabaseConnection; + | TSupabaseConnection + | TOktaConnection; export type TAvailableAppConnection = Pick; @@ -172,4 +178,5 @@ export type TAppConnectionMap = { [AppConnection.Railway]: TRailwayConnection; [AppConnection.Checkly]: TChecklyConnection; [AppConnection.Supabase]: TSupabaseConnection; + [AppConnection.Okta]: TOktaConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/okta-connection.ts b/frontend/src/hooks/api/appConnections/types/okta-connection.ts new file mode 100644 index 000000000..a622388b4 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/okta-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum OktaConnectionMethod { + ApiToken = "api-token" +} + +export type TOktaConnection = TRootAppConnection & { app: AppConnection.Okta } & { + method: OktaConnectionMethod.ApiToken; + credentials: { + instanceUrl: string; + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index bb2765ffd..be692cee3 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -6,7 +6,8 @@ export enum SecretRotation { Auth0ClientSecret = "auth0-client-secret", AzureClientSecret = "azure-client-secret", LdapPassword = "ldap-password", - AwsIamUserSecret = "aws-iam-user-secret" + AwsIamUserSecret = "aws-iam-user-secret", + OktaClientSecret = "okta-client-secret" } export enum SecretRotationStatus { diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index e4b3b6ee1..06783944b 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -35,6 +35,11 @@ import { TMySqlCredentialsRotation, TMySqlCredentialsRotationGeneratedCredentialsResponse } from "./mysql-credentials-rotation"; +import { + TOktaClientSecretRotation, + TOktaClientSecretRotationGeneratedCredentialsResponse, + TOktaClientSecretRotationOption +} from "./okta-client-secret-rotation"; import { TOracleDBCredentialsRotation, TOracleDBCredentialsRotationGeneratedCredentialsResponse @@ -49,6 +54,7 @@ export type TSecretRotationV2 = ( | TAzureClientSecretRotation | TLdapPasswordRotation | TAwsIamUserSecretRotation + | TOktaClientSecretRotation ) & { secrets: (SecretV3RawSanitized | null)[]; }; @@ -58,7 +64,8 @@ export type TSecretRotationV2Option = | TAuth0ClientSecretRotationOption | TAzureClientSecretRotationOption | TLdapPasswordRotationOption - | TAwsIamUserSecretRotationOption; + | TAwsIamUserSecretRotationOption + | TOktaClientSecretRotationOption; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; @@ -72,7 +79,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse = | TAuth0ClientSecretRotationGeneratedCredentialsResponse | TAzureClientSecretRotationGeneratedCredentialsResponse | TLdapPasswordRotationGeneratedCredentialsResponse - | TAwsIamUserSecretRotationGeneratedCredentialsResponse; + | TAwsIamUserSecretRotationGeneratedCredentialsResponse + | TOktaClientSecretRotationGeneratedCredentialsResponse; export type TCreateSecretRotationV2DTO = DiscriminativePick< TSecretRotationV2, @@ -124,6 +132,7 @@ export type TSecretRotationOptionMap = { [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption; [SecretRotation.LdapPassword]: TLdapPasswordRotationOption; [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption; + [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption; }; export type TSecretRotationGeneratedCredentialsResponseMap = { @@ -135,4 +144,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = { [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse; [SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse; [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse; + [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse; }; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/okta-client-secret-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/okta-client-secret-rotation.ts new file mode 100644 index 000000000..2884f9b29 --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/okta-client-secret-rotation.ts @@ -0,0 +1,37 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TOktaClientSecretRotation = TSecretRotationV2Base & { + type: SecretRotation.OktaClientSecret; + parameters: { + clientId: string; + }; + secretsMapping: { + clientId: string; + clientSecret: string; + }; +}; + +export type TOktaClientSecretRotationGeneratedCredentials = { + clientId: string; + clientSecret: string; +}; + +export type TOktaClientSecretRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.OktaClientSecret, + TOktaClientSecretRotationGeneratedCredentials + >; + +export type TOktaClientSecretRotationOption = { + name: string; + type: SecretRotation.OktaClientSecret; + connection: AppConnection.Okta; + template: { + secretsMapping: TOktaClientSecretRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 45a774780..b1181a68b 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -33,6 +33,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { OCIConnectionForm } from "./OCIConnectionForm"; +import { OktaConnectionForm } from "./OktaConnectionForm"; import { OracleDBConnectionForm } from "./OracleDBConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { RailwayConnectionForm } from "./RailwayConnectionForm"; @@ -149,6 +150,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.Okta: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -253,6 +256,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.Okta: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OktaConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OktaConnectionForm.tsx new file mode 100644 index 000000000..0373d1e0e --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OktaConnectionForm.tsx @@ -0,0 +1,157 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { OktaConnectionMethod, TOktaConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TOktaConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Okta) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(OktaConnectionMethod.ApiToken), + credentials: z.object({ + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .min(1, "Instance URL required") + .max(255), + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .regex(/^00[a-zA-Z0-9_-]{40}$/, "Invalid Okta API Token format") + }) + }) +]); + +type FormData = z.infer; + +export const OktaConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Okta, + method: OktaConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +};