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 { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router";
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter
};
@@ -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 { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -23,7 +24,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
Auth0ClientSecretRotationListItemSchema,
AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema
LdapPasswordRotationListItemSchema,
OktaClientSecretRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
@@ -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",
AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password"
LdapPassword = "ldap-password",
OktaClientSecret = "okta-client-secret"
}
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 { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -30,7 +31,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_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 = () => {
@@ -9,7 +9,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
[SecretRotation.LdapPassword]: "LDAP Password"
[SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.OktaClientSecret]: "Okta Client Secret"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
@@ -20,5 +21,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[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 { 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";
export type TSecretRotationV2ServiceFactoryDep = {
@@ -128,7 +129,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory 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 = ({
@@ -46,6 +46,13 @@ import {
TMySqlCredentialsRotationListItem,
TMySqlCredentialsRotationWithConnection
} from "./mysql-credentials";
import {
TOktaClientSecretRotation,
TOktaClientSecretRotationGeneratedCredentials,
TOktaClientSecretRotationInput,
TOktaClientSecretRotationListItem,
TOktaClientSecretRotationWithConnection
} from "./okta-client-secret";
import {
TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationInput,
@@ -69,7 +76,8 @@ export type TSecretRotationV2 =
| TAuth0ClientSecretRotation
| TAzureClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation;
| TAwsIamUserSecretRotation
| TOktaClientSecretRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
@@ -79,14 +87,16 @@ export type TSecretRotationV2WithConnection =
| TAuth0ClientSecretRotationWithConnection
| TAzureClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
| TAwsIamUserSecretRotationWithConnection
| TOktaClientSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials
| TAzureClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
| TAwsIamUserSecretRotationGeneratedCredentials
| TOktaClientSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
@@ -96,7 +106,8 @@ export type TSecretRotationV2Input =
| TAuth0ClientSecretRotationInput
| TAzureClientSecretRotationInput
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput;
| TAwsIamUserSecretRotationInput
| TOktaClientSecretRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
@@ -106,7 +117,8 @@ export type TSecretRotationV2ListItem =
| TAuth0ClientSecretRotationListItem
| TAzureClientSecretRotationListItem
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem;
| TAwsIamUserSecretRotationListItem
| TOktaClientSecretRotationListItem;
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 { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-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 { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
@@ -17,5 +18,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
Auth0ClientSecretRotationSchema,
AzureClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
AwsIamUserSecretRotationSchema,
OktaClientSecretRotationSchema
]);
+11
View File
@@ -2289,6 +2289,10 @@ export const AppConnections = {
SUPABASE: {
accessKey: "The Key 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: {
userName: "The name of the client to rotate credentials for.",
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: {
@@ -2616,6 +2623,10 @@ export const SecretRotations = {
AWS_IAM_USER_SECRET: {
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."
},
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 { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
@@ -138,7 +139,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options
...SanitizedSupabaseConnectionSchema.options,
...SanitizedOktaConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -175,7 +177,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema
SupabaseConnectionListItemSchema,
OktaConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -25,6 +25,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
@@ -72,5 +73,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter,
[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",
Bitbucket = "bitbucket",
Checkly = "checkly",
Supabase = "supabase"
Supabase = "supabase",
Okta = "okta"
}
export enum AWSRegion {
@@ -92,6 +92,7 @@ import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnection
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { RenderConnectionMethod } from "./render/render-connection-enums";
@@ -155,7 +156,8 @@ export const listAppConnectionOptions = () => {
getRailwayConnectionListItem(),
getBitbucketConnectionListItem(),
getChecklyConnectionListItem(),
getSupabaseConnectionListItem()
getSupabaseConnectionListItem(),
getOktaConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -241,7 +243,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
@@ -280,6 +283,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case CloudflareConnectionMethod.APIToken:
case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
@@ -367,7 +371,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
[AppConnection.Supabase]: platformManagedCredentialsNotSupported
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
@@ -34,7 +34,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Checkly]: "Checkly",
[AppConnection.Supabase]: "Supabase"
[AppConnection.Supabase]: "Supabase",
[AppConnection.Okta]: "Okta"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -71,5 +72,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: 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 { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service";
@@ -142,7 +144,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -603,6 +606,7 @@ export const appConnectionServiceFactory = ({
railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById),
checkly: checklyConnectionService(connectAppConnectionById),
supabase: supabaseConnectionService(connectAppConnectionById)
supabase: supabaseConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById)
};
};
@@ -143,6 +143,12 @@ import {
} from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql";
import {
TOktaConnection,
TOktaConnectionConfig,
TOktaConnectionInput,
TValidateOktaConnectionCredentialsSchema
} from "./okta";
import {
TPostgresConnection,
TPostgresConnectionInput,
@@ -232,6 +238,7 @@ export type TAppConnection = { id: string } & (
| TRailwayConnection
| TChecklyConnection
| TSupabaseConnection
| TOktaConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -273,6 +280,7 @@ export type TAppConnectionInput = { id: string } & (
| TRailwayConnectionInput
| TChecklyConnectionInput
| TSupabaseConnectionInput
| TOktaConnectionInput
);
export type TSqlConnectionInput =
@@ -321,7 +329,8 @@ export type TAppConnectionConfig =
| TZabbixConnectionConfig
| TRailwayConnectionConfig
| TChecklyConnectionConfig
| TSupabaseConnectionConfig;
| TSupabaseConnectionConfig
| TOktaConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -357,7 +366,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema
| TValidateSupabaseConnectionCredentialsSchema;
| TValidateSupabaseConnectionCredentialsSchema
| TValidateOktaConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -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
};