Merge pull request #4192 from Infisical/ENG-3139

feat(app-connection, secret-rotation): Okta App Connection + Okta Client Secret Rotation
This commit is contained in:
x032205
2025-07-21 11:00:38 -04:00
committed by GitHub
85 changed files with 1630 additions and 51 deletions
@@ -6,6 +6,7 @@ import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-r
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerMySqlCredentialsRotationRouter } from "./mysql-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 { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-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.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter, [SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter, [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter
}; };
@@ -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
});
@@ -7,6 +7,7 @@ import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-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 { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-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"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -23,7 +24,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
Auth0ClientSecretRotationListItemSchema, Auth0ClientSecretRotationListItemSchema,
AzureClientSecretRotationListItemSchema, AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema, AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema LdapPasswordRotationListItemSchema,
OktaClientSecretRotationListItemSchema
]); ]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
@@ -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";
@@ -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"
}
}
};
@@ -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<TOktaClientSecret>(
`${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<TOktaClientSecret[]>(
`${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<boolean> => {
const instanceUrl = await getOktaInstanceUrl(connection);
try {
const { data } = await request.get<TOktaClientSecret>(
`${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<TOktaClientSecretRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
/**
* Revokes a list of credentials.
*/
const revokeCredentials: TRotationFactoryRevokeCredentials<TOktaClientSecretRotationGeneratedCredentials> = 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<TOktaClientSecretRotationGeneratedCredentials> = 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<TOktaClientSecretRotationGeneratedCredentials> = ({
clientSecret
}) => [
{ key: secretsMapping.clientId, value: clientId },
{ key: secretsMapping.clientSecret, value: clientSecret }
];
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};
@@ -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
});
@@ -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<typeof OktaClientSecretRotationSchema>;
export type TOktaClientSecretRotationInput = z.infer<typeof CreateOktaClientSecretRotationSchema>;
export type TOktaClientSecretRotationListItem = z.infer<typeof OktaClientSecretRotationListItemSchema>;
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;
}
@@ -6,7 +6,8 @@ export enum SecretRotation {
Auth0ClientSecret = "auth0-client-secret", Auth0ClientSecret = "auth0-client-secret",
AzureClientSecret = "azure-client-secret", AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret", AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password" LdapPassword = "ldap-password",
OktaClientSecret = "okta-client-secret"
} }
export enum SecretRotationStatus { export enum SecretRotationStatus {
@@ -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 { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-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 { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -30,7 +31,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION, [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION, [SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION, [SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION [SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION
}; };
export const listSecretRotationOptions = () => { export const listSecretRotationOptions = () => {
@@ -9,7 +9,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.AzureClientSecret]: "Azure Client Secret", [SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User 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<SecretRotation, AppConnection> = { export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
@@ -20,5 +21,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets, [SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS, [SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.LdapPassword]: AppConnection.LDAP [SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.OktaClientSecret]: AppConnection.Okta
}; };
@@ -83,6 +83,7 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns"; import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = { export type TSecretRotationV2ServiceFactoryDep = {
@@ -128,7 +129,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation [SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation
}; };
export const secretRotationV2ServiceFactory = ({ export const secretRotationV2ServiceFactory = ({
@@ -46,6 +46,13 @@ import {
TMySqlCredentialsRotationListItem, TMySqlCredentialsRotationListItem,
TMySqlCredentialsRotationWithConnection TMySqlCredentialsRotationWithConnection
} from "./mysql-credentials"; } from "./mysql-credentials";
import {
TOktaClientSecretRotation,
TOktaClientSecretRotationGeneratedCredentials,
TOktaClientSecretRotationInput,
TOktaClientSecretRotationListItem,
TOktaClientSecretRotationWithConnection
} from "./okta-client-secret";
import { import {
TOracleDBCredentialsRotation, TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationInput, TOracleDBCredentialsRotationInput,
@@ -69,7 +76,8 @@ export type TSecretRotationV2 =
| TAuth0ClientSecretRotation | TAuth0ClientSecretRotation
| TAzureClientSecretRotation | TAzureClientSecretRotation
| TLdapPasswordRotation | TLdapPasswordRotation
| TAwsIamUserSecretRotation; | TAwsIamUserSecretRotation
| TOktaClientSecretRotation;
export type TSecretRotationV2WithConnection = export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection | TPostgresCredentialsRotationWithConnection
@@ -79,14 +87,16 @@ export type TSecretRotationV2WithConnection =
| TAuth0ClientSecretRotationWithConnection | TAuth0ClientSecretRotationWithConnection
| TAzureClientSecretRotationWithConnection | TAzureClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection | TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection; | TAwsIamUserSecretRotationWithConnection
| TOktaClientSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials = export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials | TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials | TAuth0ClientSecretRotationGeneratedCredentials
| TAzureClientSecretRotationGeneratedCredentials | TAzureClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials | TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials; | TAwsIamUserSecretRotationGeneratedCredentials
| TOktaClientSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input = export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput | TPostgresCredentialsRotationInput
@@ -96,7 +106,8 @@ export type TSecretRotationV2Input =
| TAuth0ClientSecretRotationInput | TAuth0ClientSecretRotationInput
| TAzureClientSecretRotationInput | TAzureClientSecretRotationInput
| TLdapPasswordRotationInput | TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput; | TAwsIamUserSecretRotationInput
| TOktaClientSecretRotationInput;
export type TSecretRotationV2ListItem = export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem | TPostgresCredentialsRotationListItem
@@ -106,7 +117,8 @@ export type TSecretRotationV2ListItem =
| TAuth0ClientSecretRotationListItem | TAuth0ClientSecretRotationListItem
| TAzureClientSecretRotationListItem | TAzureClientSecretRotationListItem
| TLdapPasswordRotationListItem | TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem; | TAwsIamUserSecretRotationListItem
| TOktaClientSecretRotationListItem;
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined; export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
@@ -6,6 +6,7 @@ import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotatio
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials"; import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
@@ -17,5 +18,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
Auth0ClientSecretRotationSchema, Auth0ClientSecretRotationSchema,
AzureClientSecretRotationSchema, AzureClientSecretRotationSchema,
LdapPasswordRotationSchema, LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema AwsIamUserSecretRotationSchema,
OktaClientSecretRotationSchema
]); ]);
+11
View File
@@ -2289,6 +2289,10 @@ export const AppConnections = {
SUPABASE: { SUPABASE: {
accessKey: "The Key used to access Supabase.", accessKey: "The Key used to access Supabase.",
instanceUrl: "The URL used to access Supabase." instanceUrl: "The URL used to access Supabase."
},
OKTA: {
instanceUrl: "The URL used to access your Okta organization.",
apiToken: "The API token used to authenticate with Okta."
} }
} }
}; };
@@ -2594,6 +2598,9 @@ export const SecretRotations = {
AWS_IAM_USER_SECRET: { AWS_IAM_USER_SECRET: {
userName: "The name of the client to rotate credentials for.", userName: "The name of the client to rotate credentials for.",
region: "The AWS region the client is present in." region: "The AWS region the client is present in."
},
OKTA_CLIENT_SECRET: {
clientId: "The ID of the Okta Application to rotate the client secret for."
} }
}, },
SECRETS_MAPPING: { SECRETS_MAPPING: {
@@ -2616,6 +2623,10 @@ export const SecretRotations = {
AWS_IAM_USER_SECRET: { AWS_IAM_USER_SECRET: {
accessKeyId: "The name of the secret that the access key ID will be mapped to.", accessKeyId: "The name of the secret that the access key ID will be mapped to.",
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to." secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
},
OKTA_CLIENT_SECRET: {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
} }
} }
}; };
@@ -71,6 +71,7 @@ import {
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql"; import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
import { import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema SanitizedPostgresConnectionSchema
@@ -138,7 +139,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedZabbixConnectionSchema.options, ...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options, ...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options, ...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options ...SanitizedSupabaseConnectionSchema.options,
...SanitizedOktaConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -175,7 +177,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
ZabbixConnectionListItemSchema, ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema, RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema, ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema SupabaseConnectionListItemSchema,
OktaConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -25,6 +25,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router"; import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router"; import { registerRenderConnectionRouter } from "./render-connection-router";
@@ -72,5 +73,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Zabbix]: registerZabbixConnectionRouter, [AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter, [AppConnection.Railway]: registerRailwayConnectionRouter,
[AppConnection.Checkly]: registerChecklyConnectionRouter, [AppConnection.Checkly]: registerChecklyConnectionRouter,
[AppConnection.Supabase]: registerSupabaseConnectionRouter [AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter
}; };
@@ -0,0 +1,52 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateOktaConnectionSchema,
SanitizedOktaConnectionSchema,
UpdateOktaConnectionSchema
} from "@app/services/app-connection/okta";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerOktaConnectionRouter = async (server: FastifyZodProvider) => {
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 };
}
});
};
@@ -32,7 +32,8 @@ export enum AppConnection {
Railway = "railway", Railway = "railway",
Bitbucket = "bitbucket", Bitbucket = "bitbucket",
Checkly = "checkly", Checkly = "checkly",
Supabase = "supabase" Supabase = "supabase",
Okta = "okta"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -92,6 +92,7 @@ import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnection
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { RenderConnectionMethod } from "./render/render-connection-enums"; import { RenderConnectionMethod } from "./render/render-connection-enums";
@@ -155,7 +156,8 @@ export const listAppConnectionOptions = () => {
getRailwayConnectionListItem(), getRailwayConnectionListItem(),
getBitbucketConnectionListItem(), getBitbucketConnectionListItem(),
getChecklyConnectionListItem(), getChecklyConnectionListItem(),
getSupabaseConnectionListItem() getSupabaseConnectionListItem(),
getOktaConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -241,7 +243,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Checkly]: validateChecklyConnectionCredentials 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); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
@@ -280,6 +283,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case CloudflareConnectionMethod.APIToken: case CloudflareConnectionMethod.APIToken:
case BitbucketConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
@@ -367,7 +371,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Checkly]: platformManagedCredentialsNotSupported, [AppConnection.Checkly]: platformManagedCredentialsNotSupported,
[AppConnection.Supabase]: platformManagedCredentialsNotSupported [AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (
@@ -34,7 +34,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Railway]: "Railway", [AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Checkly]: "Checkly", [AppConnection.Checkly]: "Checkly",
[AppConnection.Supabase]: "Supabase" [AppConnection.Supabase]: "Supabase",
[AppConnection.Okta]: "Okta"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -71,5 +72,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Railway]: AppConnectionPlanType.Regular, [AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular, [AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Checkly]: AppConnectionPlanType.Regular, [AppConnection.Checkly]: AppConnectionPlanType.Regular,
[AppConnection.Supabase]: AppConnectionPlanType.Regular [AppConnection.Supabase]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular
}; };
@@ -79,6 +79,8 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser
import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql"; import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRailwayConnectionCredentialsSchema } from "./railway"; import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service"; import { railwayConnectionService } from "./railway/railway-connection-service";
@@ -142,7 +144,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema, [AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema, [AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema, [AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema [AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -603,6 +606,7 @@ export const appConnectionServiceFactory = ({
railway: railwayConnectionService(connectAppConnectionById), railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById), bitbucket: bitbucketConnectionService(connectAppConnectionById),
checkly: checklyConnectionService(connectAppConnectionById), checkly: checklyConnectionService(connectAppConnectionById),
supabase: supabaseConnectionService(connectAppConnectionById) supabase: supabaseConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById)
}; };
}; };
@@ -143,6 +143,12 @@ import {
} from "./ldap"; } from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql"; import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql";
import {
TOktaConnection,
TOktaConnectionConfig,
TOktaConnectionInput,
TValidateOktaConnectionCredentialsSchema
} from "./okta";
import { import {
TPostgresConnection, TPostgresConnection,
TPostgresConnectionInput, TPostgresConnectionInput,
@@ -232,6 +238,7 @@ export type TAppConnection = { id: string } & (
| TRailwayConnection | TRailwayConnection
| TChecklyConnection | TChecklyConnection
| TSupabaseConnection | TSupabaseConnection
| TOktaConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -273,6 +280,7 @@ export type TAppConnectionInput = { id: string } & (
| TRailwayConnectionInput | TRailwayConnectionInput
| TChecklyConnectionInput | TChecklyConnectionInput
| TSupabaseConnectionInput | TSupabaseConnectionInput
| TOktaConnectionInput
); );
export type TSqlConnectionInput = export type TSqlConnectionInput =
@@ -321,7 +329,8 @@ export type TAppConnectionConfig =
| TZabbixConnectionConfig | TZabbixConnectionConfig
| TRailwayConnectionConfig | TRailwayConnectionConfig
| TChecklyConnectionConfig | TChecklyConnectionConfig
| TSupabaseConnectionConfig; | TSupabaseConnectionConfig
| TOktaConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -357,7 +366,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateZabbixConnectionCredentialsSchema | TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema | TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema | TValidateChecklyConnectionCredentialsSchema
| TValidateSupabaseConnectionCredentialsSchema; | TValidateSupabaseConnectionCredentialsSchema
| TValidateOktaConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -0,0 +1,4 @@
export * from "./okta-connection-enums";
export * from "./okta-connection-fns";
export * from "./okta-connection-schemas";
export * from "./okta-connection-types";
@@ -0,0 +1,3 @@
export enum OktaConnectionMethod {
ApiToken = "api-token"
}
@@ -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<TOktaApp[]>(`${instanceUrl}/api/v1/apps`, {
headers: {
Accept: "application/json",
Authorization: `SSWS ${apiToken}`
}
});
return data.filter((app) => app.status === "ACTIVE" && app.name === "oidc_client");
};
@@ -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()
});
@@ -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<TOktaConnection>;
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
};
};
@@ -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<typeof OktaConnectionSchema>;
export type TOktaConnectionInput = z.infer<typeof CreateOktaConnectionSchema> & {
app: AppConnection.Okta;
};
export type TValidateOktaConnectionCredentialsSchema = typeof ValidateOktaConnectionCredentialsSchema;
export type TOktaConnectionConfig = DiscriminativePick<TOktaConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TOktaApp = {
id: string;
label: string;
status: "ACTIVE" | "INACTIVE";
name: "oidc_client"; // "oidc_client" or other types
};
@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/okta/available"
---
@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/okta"
---
<Note>
Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/okta/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/okta/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/okta/connection-name/{connectionName}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/okta"
---
@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/okta/{connectionId}"
---
<Note>
Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials.
</Note>
@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/okta-client-secret"
---
<Note>
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.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/okta-client-secret/{rotationId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/okta-client-secret/rotation-name/{rotationName}"
---
@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}/generated-credentials"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/okta-client-secret"
---
@@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/okta-client-secret/{rotationId}/rotate-secrets"
---
@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/okta-client-secret/{rotationId}"
---
<Note>
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.
</Note>
+47 -5
View File
@@ -78,7 +78,10 @@
}, },
{ {
"group": "Infisical SSH", "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)", "group": "Key Management (KMS)",
@@ -146,6 +149,7 @@
"documentation/platform/secret-rotation/ldap-password", "documentation/platform/secret-rotation/ldap-password",
"documentation/platform/secret-rotation/mssql-credentials", "documentation/platform/secret-rotation/mssql-credentials",
"documentation/platform/secret-rotation/mysql-credentials", "documentation/platform/secret-rotation/mysql-credentials",
"documentation/platform/secret-rotation/okta-client-secret",
"documentation/platform/secret-rotation/oracledb-credentials", "documentation/platform/secret-rotation/oracledb-credentials",
"documentation/platform/secret-rotation/postgres-credentials" "documentation/platform/secret-rotation/postgres-credentials"
] ]
@@ -375,7 +379,10 @@
}, },
{ {
"group": "Architecture", "group": "Architecture",
"pages": ["internals/architecture/components", "internals/architecture/cloud"] "pages": [
"internals/architecture/components",
"internals/architecture/cloud"
]
}, },
"internals/security", "internals/security",
"internals/service-tokens" "internals/service-tokens"
@@ -481,6 +488,7 @@
"integrations/app-connections/mssql", "integrations/app-connections/mssql",
"integrations/app-connections/mysql", "integrations/app-connections/mysql",
"integrations/app-connections/oci", "integrations/app-connections/oci",
"integrations/app-connections/okta",
"integrations/app-connections/oracledb", "integrations/app-connections/oracledb",
"integrations/app-connections/postgres", "integrations/app-connections/postgres",
"integrations/app-connections/railway", "integrations/app-connections/railway",
@@ -551,7 +559,10 @@
"integrations/cloud/gcp-secret-manager", "integrations/cloud/gcp-secret-manager",
{ {
"group": "Cloudflare", "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/terraform-cloud",
"integrations/cloud/databricks", "integrations/cloud/databricks",
@@ -663,7 +674,11 @@
"cli/commands/reset", "cli/commands/reset",
{ {
"group": "infisical scan", "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": [ "pages": [
{ {
"group": "Kubernetes", "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/create",
"api-reference/endpoints/dynamic-secrets/update", "api-reference/endpoints/dynamic-secrets/update",
@@ -1093,6 +1110,19 @@
"api-reference/endpoints/secret-rotations/mysql-credentials/update" "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", "group": "OracleDB Credentials",
"pages": [ "pages": [
@@ -1496,6 +1526,18 @@
"api-reference/endpoints/app-connections/oci/delete" "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", "group": "OracleDB",
"pages": [ "pages": [
@@ -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
<Tabs>
<Tab title="Infisical UI">
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)
</Tab>
<Tab title="API">
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": "..."
}
}
}
```
</Tab>
</Tabs>
Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

@@ -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
<Steps>
<Step title="Create 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)
</Step>
<Step title="Provide Info">
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)
</Step>
<Step title="Copy Token">
Copy the token from the modal for later steps.
![Copy Token](/images/app-connections/okta/step-3.png)
</Step>
</Steps>
## Create Okta Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
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)
</Step>
<Step title="Select Okta Connection">
Click the **Add Connection** button and select **Okta** from the list of available connections.
</Step>
<Step title="Fill out Connection Modal">
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)
</Step>
<Step title="Connection Created">
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)
</Step>
</Steps>
</Tab>
<Tab title="API">
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": "<YOUR-API-TOKEN>"
}
}'
```
### 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"
}
}
}
```
</Tab>
</Tabs>
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@@ -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 (
<ViewRotationGeneratedCredentialsDisplay
activeCredentials={
<>
<CredentialDisplay label="Client ID">{activeCredentials?.clientId}</CredentialDisplay>
<CredentialDisplay isSensitive label="Client Secret">
{activeCredentials?.clientSecret}
</CredentialDisplay>
</>
}
inactiveCredentials={
<>
<CredentialDisplay label="Client ID">{inactiveCredentials?.clientId}</CredentialDisplay>
<CredentialDisplay isSensitive label="Client Secret">
{inactiveCredentials?.clientSecret}
</CredentialDisplay>
</>
}
/>
);
};
@@ -22,6 +22,7 @@ import {
import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared"; import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared";
import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials"; import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials";
import { ViewOktaClientSecretRotationGeneratedCredentials } from "./ViewOktaClientSecretRotationGeneratedCredentials";
type Props = { type Props = {
secretRotation?: TSecretRotationV2; secretRotation?: TSecretRotationV2;
@@ -99,6 +100,13 @@ const Content = ({ secretRotation }: ContentProps) => {
/> />
); );
break; break;
case SecretRotation.OktaClientSecret:
Component = (
<ViewOktaClientSecretRotationGeneratedCredentials
generatedCredentialsResponse={generatedCredentialsResponse}
/>
);
break;
default: default:
throw new Error("Unhandled View Generated Credential Rotation Type"); throw new Error("Unhandled View Generated Credential Rotation Type");
} }
@@ -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 (
<Controller
name="parameters.clientId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="OpenID Connect Application"
>
<FilterableSelect
menuPlacement="top"
isLoading={isAppsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={apps?.find((app) => app.id === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TOktaApp>)?.id ?? null);
setValue("parameters.clientId", (option as SingleValue<TOktaApp>)?.id ?? "");
}}
options={apps}
placeholder="Select an application..."
getOptionLabel={(option) => option.label}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
);
};
@@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRo
import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields"; import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields";
import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields"; import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields";
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields"; import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
import { OktaClientSecretRotationParametersFields } from "./OktaClientSecretRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared"; import { SqlCredentialsRotationParametersFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = { const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
@@ -17,7 +18,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields, [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields, [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields
}; };
export const SecretRotationV2ParametersFields = () => { export const SecretRotationV2ParametersFields = () => {
@@ -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 (
<>
<SecretRotationReviewSection label="Parameters">
<GenericFieldLabel label="App ID">{parameters.clientId}</GenericFieldLabel>
</SecretRotationReviewSection>
<SecretRotationReviewSection label="Secrets Mapping">
<GenericFieldLabel label="Client ID">{clientId}</GenericFieldLabel>
<GenericFieldLabel label="Client Secret">{clientSecret}</GenericFieldLabel>
</SecretRotationReviewSection>
</>
);
};
@@ -10,6 +10,7 @@ import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotati
import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields"; import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields";
import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields"; import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields";
import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields"; import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields";
import { OktaClientSecretRotationReviewFields } from "./OktaClientSecretRotationReviewFields";
import { SqlCredentialsRotationReviewFields } from "./shared"; import { SqlCredentialsRotationReviewFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = { const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
@@ -20,7 +21,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields, [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields, [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields,
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields
}; };
export const SecretRotationV2ReviewFields = () => { export const SecretRotationV2ReviewFields = () => {
@@ -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: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.clientId}
/>
</FormControl>
)}
control={control}
name="secretsMapping.clientId"
/>
)
},
{
name: "Client Secret",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.clientSecret}
/>
</FormControl>
)}
control={control}
name="secretsMapping.clientSecret"
/>
)
}
];
return <SecretsMappingTable items={items} />;
};
@@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecr
import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields"; import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields";
import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields"; import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields";
import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields"; import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields";
import { OktaClientSecretRotationSecretsMappingFields } from "./OktaClientSecretRotationSecretsMappingFields";
import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = { const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
@@ -17,7 +18,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields, [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields, [SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields, [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields,
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields
}; };
export const SecretRotationV2SecretsMappingFields = () => { export const SecretRotationV2SecretsMappingFields = () => {
@@ -10,6 +10,7 @@ import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotati
import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; 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"; import { OracleDBCredentialsRotationSchema } from "./oracledb-credentials-rotation-schema";
export const SecretRotationV2FormSchema = (isUpdate: boolean) => export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
@@ -23,7 +24,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
MySqlCredentialsRotationSchema, MySqlCredentialsRotationSchema,
OracleDBCredentialsRotationSchema, OracleDBCredentialsRotationSchema,
LdapPasswordRotationSchema, LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema AwsIamUserSecretRotationSchema,
OktaClientSecretRotationSchema
]), ]),
z.object({ id: z.string().optional() }) z.object({ id: z.string().optional() })
) )
@@ -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);
+11 -8
View File
@@ -17,7 +17,9 @@ import {
AzureClientSecretsConnectionMethod, AzureClientSecretsConnectionMethod,
AzureDevOpsConnectionMethod, AzureDevOpsConnectionMethod,
AzureKeyVaultConnectionMethod, AzureKeyVaultConnectionMethod,
BitbucketConnectionMethod,
CamundaConnectionMethod, CamundaConnectionMethod,
ChecklyConnectionMethod,
CloudflareConnectionMethod, CloudflareConnectionMethod,
DatabricksConnectionMethod, DatabricksConnectionMethod,
FlyioConnectionMethod, FlyioConnectionMethod,
@@ -26,13 +28,19 @@ import {
GitHubRadarConnectionMethod, GitHubRadarConnectionMethod,
GitLabConnectionMethod, GitLabConnectionMethod,
HCVaultConnectionMethod, HCVaultConnectionMethod,
HerokuConnectionMethod,
HumanitecConnectionMethod, HumanitecConnectionMethod,
LdapConnectionMethod, LdapConnectionMethod,
MsSqlConnectionMethod, MsSqlConnectionMethod,
MySqlConnectionMethod, MySqlConnectionMethod,
OCIConnectionMethod,
OktaConnectionMethod,
OnePassConnectionMethod, OnePassConnectionMethod,
OracleDBConnectionMethod, OracleDBConnectionMethod,
PostgresConnectionMethod, PostgresConnectionMethod,
RailwayConnectionMethod,
RenderConnectionMethod,
SupabaseConnectionMethod,
TAppConnection, TAppConnection,
TeamCityConnectionMethod, TeamCityConnectionMethod,
TerraformCloudConnectionMethod, TerraformCloudConnectionMethod,
@@ -40,13 +48,6 @@ import {
WindmillConnectionMethod, WindmillConnectionMethod,
ZabbixConnectionMethod ZabbixConnectionMethod
} from "@app/hooks/api/appConnections/types"; } 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< export const APP_CONNECTION_MAP: Record<
AppConnection, AppConnection,
@@ -98,7 +99,8 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" }, [AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
[AppConnection.Checkly]: { name: "Checkly", image: "Checkly.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"]) => { export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -132,6 +134,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case CloudflareConnectionMethod.ApiToken: case CloudflareConnectionMethod.ApiToken:
case BitbucketConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey }; return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
+9 -2
View File
@@ -44,6 +44,11 @@ export const SECRET_ROTATION_MAP: Record<
name: "AWS IAM User Secret", name: "AWS IAM User Secret",
image: "Amazon Web Services.png", image: "Amazon Web Services.png",
size: 50 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, AppConnectio
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets, [SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.LdapPassword]: AppConnection.LDAP, [SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS [SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.OktaClientSecret]: AppConnection.Okta
}; };
// if a rotation can potentially have downtime due to rotating a single credential set this to false // if a rotation can potentially have downtime due to rotating a single credential set this to false
@@ -67,7 +73,8 @@ export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
[SecretRotation.Auth0ClientSecret]: false, [SecretRotation.Auth0ClientSecret]: false,
[SecretRotation.AzureClientSecret]: true, [SecretRotation.AzureClientSecret]: true,
[SecretRotation.LdapPassword]: false, [SecretRotation.LdapPassword]: false,
[SecretRotation.AwsIamUserSecret]: true [SecretRotation.AwsIamUserSecret]: true,
[SecretRotation.OktaClientSecret]: true
}; };
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
@@ -32,5 +32,6 @@ export enum AppConnection {
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",
Checkly = "checkly", Checkly = "checkly",
Supabase = "supabase" Supabase = "supabase",
Okta = "okta"
} }
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -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<typeof oktaConnectionKeys.listApps>
>,
"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
});
};
@@ -0,0 +1,4 @@
export type TOktaApp = {
id: string;
label: string;
};
@@ -152,6 +152,10 @@ export type TSupabaseConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Supabase; app: AppConnection.Supabase;
}; };
export type TOktaConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Okta;
};
export type TAppConnectionOption = export type TAppConnectionOption =
| TAwsConnectionOption | TAwsConnectionOption
| TGitHubConnectionOption | TGitHubConnectionOption
@@ -183,7 +187,8 @@ export type TAppConnectionOption =
| TBitbucketConnectionOption | TBitbucketConnectionOption
| TZabbixConnectionOption | TZabbixConnectionOption
| TRailwayConnectionOption | TRailwayConnectionOption
| TChecklyConnectionOption; | TChecklyConnectionOption
| TOktaConnectionOption;
export type TAppConnectionOptionMap = { export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption; [AppConnection.AWS]: TAwsConnectionOption;
@@ -220,4 +225,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Railway]: TRailwayConnectionOption; [AppConnection.Railway]: TRailwayConnectionOption;
[AppConnection.Checkly]: TChecklyConnectionOption; [AppConnection.Checkly]: TChecklyConnectionOption;
[AppConnection.Supabase]: TSupabaseConnectionOption; [AppConnection.Supabase]: TSupabaseConnectionOption;
[AppConnection.Okta]: TOktaConnectionOption;
}; };
@@ -24,6 +24,7 @@ import { TLdapConnection } from "./ldap-connection";
import { TMsSqlConnection } from "./mssql-connection"; import { TMsSqlConnection } from "./mssql-connection";
import { TMySqlConnection } from "./mysql-connection"; import { TMySqlConnection } from "./mysql-connection";
import { TOCIConnection } from "./oci-connection"; import { TOCIConnection } from "./oci-connection";
import { TOktaConnection } from "./okta-connection";
import { TOracleDBConnection } from "./oracledb-connection"; import { TOracleDBConnection } from "./oracledb-connection";
import { TPostgresConnection } from "./postgres-connection"; import { TPostgresConnection } from "./postgres-connection";
import { TRailwayConnection } from "./railway-connection"; import { TRailwayConnection } from "./railway-connection";
@@ -44,6 +45,7 @@ export * from "./azure-devops-connection";
export * from "./azure-key-vault-connection"; export * from "./azure-key-vault-connection";
export * from "./bitbucket-connection"; export * from "./bitbucket-connection";
export * from "./camunda-connection"; export * from "./camunda-connection";
export * from "./checkly-connection";
export * from "./cloudflare-connection"; export * from "./cloudflare-connection";
export * from "./databricks-connection"; export * from "./databricks-connection";
export * from "./flyio-connection"; export * from "./flyio-connection";
@@ -58,9 +60,12 @@ export * from "./ldap-connection";
export * from "./mssql-connection"; export * from "./mssql-connection";
export * from "./mysql-connection"; export * from "./mysql-connection";
export * from "./oci-connection"; export * from "./oci-connection";
export * from "./okta-connection";
export * from "./oracledb-connection"; export * from "./oracledb-connection";
export * from "./postgres-connection"; export * from "./postgres-connection";
export * from "./railway-connection";
export * from "./render-connection"; export * from "./render-connection";
export * from "./supabase-connection";
export * from "./teamcity-connection"; export * from "./teamcity-connection";
export * from "./terraform-cloud-connection"; export * from "./terraform-cloud-connection";
export * from "./vercel-connection"; export * from "./vercel-connection";
@@ -101,7 +106,8 @@ export type TAppConnection =
| TZabbixConnection | TZabbixConnection
| TRailwayConnection | TRailwayConnection
| TChecklyConnection | TChecklyConnection
| TSupabaseConnection; | TSupabaseConnection
| TOktaConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">; export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -172,4 +178,5 @@ export type TAppConnectionMap = {
[AppConnection.Railway]: TRailwayConnection; [AppConnection.Railway]: TRailwayConnection;
[AppConnection.Checkly]: TChecklyConnection; [AppConnection.Checkly]: TChecklyConnection;
[AppConnection.Supabase]: TSupabaseConnection; [AppConnection.Supabase]: TSupabaseConnection;
[AppConnection.Okta]: TOktaConnection;
}; };
@@ -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;
};
};
@@ -6,7 +6,8 @@ export enum SecretRotation {
Auth0ClientSecret = "auth0-client-secret", Auth0ClientSecret = "auth0-client-secret",
AzureClientSecret = "azure-client-secret", AzureClientSecret = "azure-client-secret",
LdapPassword = "ldap-password", LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret" AwsIamUserSecret = "aws-iam-user-secret",
OktaClientSecret = "okta-client-secret"
} }
export enum SecretRotationStatus { export enum SecretRotationStatus {
@@ -35,6 +35,11 @@ import {
TMySqlCredentialsRotation, TMySqlCredentialsRotation,
TMySqlCredentialsRotationGeneratedCredentialsResponse TMySqlCredentialsRotationGeneratedCredentialsResponse
} from "./mysql-credentials-rotation"; } from "./mysql-credentials-rotation";
import {
TOktaClientSecretRotation,
TOktaClientSecretRotationGeneratedCredentialsResponse,
TOktaClientSecretRotationOption
} from "./okta-client-secret-rotation";
import { import {
TOracleDBCredentialsRotation, TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationGeneratedCredentialsResponse TOracleDBCredentialsRotationGeneratedCredentialsResponse
@@ -49,6 +54,7 @@ export type TSecretRotationV2 = (
| TAzureClientSecretRotation | TAzureClientSecretRotation
| TLdapPasswordRotation | TLdapPasswordRotation
| TAwsIamUserSecretRotation | TAwsIamUserSecretRotation
| TOktaClientSecretRotation
) & { ) & {
secrets: (SecretV3RawSanitized | null)[]; secrets: (SecretV3RawSanitized | null)[];
}; };
@@ -58,7 +64,8 @@ export type TSecretRotationV2Option =
| TAuth0ClientSecretRotationOption | TAuth0ClientSecretRotationOption
| TAzureClientSecretRotationOption | TAzureClientSecretRotationOption
| TLdapPasswordRotationOption | TLdapPasswordRotationOption
| TAwsIamUserSecretRotationOption; | TAwsIamUserSecretRotationOption
| TOktaClientSecretRotationOption;
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
@@ -72,7 +79,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse =
| TAuth0ClientSecretRotationGeneratedCredentialsResponse | TAuth0ClientSecretRotationGeneratedCredentialsResponse
| TAzureClientSecretRotationGeneratedCredentialsResponse | TAzureClientSecretRotationGeneratedCredentialsResponse
| TLdapPasswordRotationGeneratedCredentialsResponse | TLdapPasswordRotationGeneratedCredentialsResponse
| TAwsIamUserSecretRotationGeneratedCredentialsResponse; | TAwsIamUserSecretRotationGeneratedCredentialsResponse
| TOktaClientSecretRotationGeneratedCredentialsResponse;
export type TCreateSecretRotationV2DTO = DiscriminativePick< export type TCreateSecretRotationV2DTO = DiscriminativePick<
TSecretRotationV2, TSecretRotationV2,
@@ -124,6 +132,7 @@ export type TSecretRotationOptionMap = {
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption; [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption;
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption; [SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption; [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption;
}; };
export type TSecretRotationGeneratedCredentialsResponseMap = { export type TSecretRotationGeneratedCredentialsResponseMap = {
@@ -135,4 +144,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse; [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse;
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse; [SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse; [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse;
}; };
@@ -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"];
};
};
@@ -33,6 +33,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm"; import { OCIConnectionForm } from "./OCIConnectionForm";
import { OktaConnectionForm } from "./OktaConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm"; import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { RailwayConnectionForm } from "./RailwayConnectionForm"; import { RailwayConnectionForm } from "./RailwayConnectionForm";
@@ -149,6 +150,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <ChecklyConnectionForm onSubmit={onSubmit} />; return <ChecklyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Supabase: case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} />; return <SupabaseConnectionForm onSubmit={onSubmit} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} />;
default: default:
throw new Error(`Unhandled App ${app}`); throw new Error(`Unhandled App ${app}`);
} }
@@ -253,6 +256,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Supabase: case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default: default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
} }
@@ -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<typeof formSchema>;
export const OktaConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Okta,
method: OktaConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Okta].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(OktaConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.instanceUrl"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Instance URL"
>
<Input {...field} placeholder="https://example.okta.com" />
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Okta"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};