Compare commits

..

9 Commits

Author SHA1 Message Date
ae1ee25687 Merge pull request #3436 from Infisical/misc/made-jwt-signature-alg-configurable-for-oidc
misc: made jwt signature alg configurable for oidc
2025-04-17 00:47:16 +08:00
4650ba9fdd Merge pull request #3397 from Infisical/auth0-connection-and-secret-rotation
Feature: Auth0 Connection and Client Secret Rotation
2025-04-16 08:19:50 -07:00
e7742afcd3 Merge pull request #3434 from Infisical/fix/improveRandomValueGeneratorUI
Improve random value generator modal UI
2025-04-16 09:58:07 -03:00
927eb0407d misc: update documentation 2025-04-16 12:33:22 +00:00
17ddb79def misc: made jwt signature alg configurable for oidc 2025-04-16 20:20:37 +08:00
5eb9a1a667 improvement: add doc additions for single credential rotations 2025-04-15 15:07:39 -07:00
03ad6f822a merge deconflict 2025-04-15 14:32:21 -07:00
98447e9402 improvements: address feedback 2025-04-15 14:22:41 -07:00
581e4b35f9 rebase 2025-04-11 12:25:26 -07:00
137 changed files with 2343 additions and 451 deletions

View File

@ -50,7 +50,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
- **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface. - **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface.
- **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more. - **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed. - **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed.
- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more. - **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more.
- **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more. - **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more.
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git. - **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git.
- **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments. - **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments.

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm"))) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.string("jwtSignatureAlgorithm").defaultTo(OIDCJWTSignatureAlgorithm.RS256).notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm")) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.dropColumn("jwtSignatureAlgorithm");
});
}
}

View File

@ -30,9 +30,10 @@ export const OidcConfigsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
lastUsed: z.date().nullable().optional(), lastUsed: z.date().nullable().optional(),
manageGroupMemberships: z.boolean().default(false),
encryptedOidcClientId: zodBuffer, encryptedOidcClientId: zodBuffer,
encryptedOidcClientSecret: zodBuffer encryptedOidcClientSecret: zodBuffer,
manageGroupMemberships: z.boolean().default(false),
jwtSignatureAlgorithm: z.string().default("RS256")
}); });
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>; export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;

View File

@ -12,7 +12,7 @@ import RedisStore from "connect-redis";
import { z } from "zod"; import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas"; import { OidcConfigsSchema } from "@app/db/schemas";
import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -30,7 +30,8 @@ const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({
orgId: true, orgId: true,
isActive: true, isActive: true,
allowedEmailDomains: true, allowedEmailDomains: true,
manageGroupMemberships: true manageGroupMemberships: true,
jwtSignatureAlgorithm: true
}); });
export const registerOidcRouter = async (server: FastifyZodProvider) => { export const registerOidcRouter = async (server: FastifyZodProvider) => {
@ -170,7 +171,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
isActive: true, isActive: true,
orgId: true, orgId: true,
allowedEmailDomains: true, allowedEmailDomains: true,
manageGroupMemberships: true manageGroupMemberships: true,
jwtSignatureAlgorithm: true
}).extend({ }).extend({
clientId: z.string(), clientId: z.string(),
clientSecret: z.string() clientSecret: z.string()
@ -225,7 +227,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
clientId: z.string().trim(), clientId: z.string().trim(),
clientSecret: z.string().trim(), clientSecret: z.string().trim(),
isActive: z.boolean(), isActive: z.boolean(),
manageGroupMemberships: z.boolean().optional() manageGroupMemberships: z.boolean().optional(),
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional()
}) })
.partial() .partial()
.merge(z.object({ orgSlug: z.string() })), .merge(z.object({ orgSlug: z.string() })),
@ -292,7 +295,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
clientSecret: z.string().trim(), clientSecret: z.string().trim(),
isActive: z.boolean(), isActive: z.boolean(),
orgSlug: z.string().trim(), orgSlug: z.string().trim(),
manageGroupMemberships: z.boolean().optional().default(false) manageGroupMemberships: z.boolean().optional().default(false),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.default(OIDCJWTSignatureAlgorithm.RS256)
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) { if (data.configurationType === OIDCConfigurationType.CUSTOM) {

View File

@ -0,0 +1,19 @@
import {
Auth0ClientSecretRotationGeneratedCredentialsSchema,
Auth0ClientSecretRotationSchema,
CreateAuth0ClientSecretRotationSchema,
UpdateAuth0ClientSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerAuth0ClientSecretRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.Auth0ClientSecret,
server,
responseSchema: Auth0ClientSecretRotationSchema,
createSchema: CreateAuth0ClientSecretRotationSchema,
updateSchema: UpdateAuth0ClientSecretRotationSchema,
generatedCredentialsSchema: Auth0ClientSecretRotationGeneratedCredentialsSchema
});

View File

@ -1,5 +1,6 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@ -10,5 +11,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
(server: FastifyZodProvider) => Promise<void> (server: FastifyZodProvider) => Promise<void>
> = { > = {
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
}; };

View File

@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-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";
@ -11,7 +12,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema, PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema
]); ]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {

View File

@ -165,7 +165,8 @@ export const oidcConfigServiceFactory = ({
allowedEmailDomains: oidcCfg.allowedEmailDomains, allowedEmailDomains: oidcCfg.allowedEmailDomains,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships: oidcCfg.manageGroupMemberships manageGroupMemberships: oidcCfg.manageGroupMemberships,
jwtSignatureAlgorithm: oidcCfg.jwtSignatureAlgorithm
}; };
}; };
@ -481,7 +482,8 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}: TUpdateOidcCfgDTO) => { }: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({
slug: orgSlug slug: orgSlug
@ -536,7 +538,8 @@ export const oidcConfigServiceFactory = ({
jwksUri, jwksUri,
isActive, isActive,
lastUsed: null, lastUsed: null,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}; };
if (clientId !== undefined) { if (clientId !== undefined) {
@ -569,7 +572,8 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}: TCreateOidcCfgDTO) => { }: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({
slug: orgSlug slug: orgSlug
@ -613,6 +617,7 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
orgId: org.id, orgId: org.id,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm,
encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob,
encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob
}); });
@ -676,7 +681,8 @@ export const oidcConfigServiceFactory = ({
const client = new issuer.Client({ const client = new issuer.Client({
client_id: oidcCfg.clientId, client_id: oidcCfg.clientId,
client_secret: oidcCfg.clientSecret, client_secret: oidcCfg.clientSecret,
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`] redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`],
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
}); });
const strategy = new OpenIdStrategy( const strategy = new OpenIdStrategy(

View File

@ -5,6 +5,12 @@ export enum OIDCConfigurationType {
DISCOVERY_URL = "discoveryURL" DISCOVERY_URL = "discoveryURL"
} }
export enum OIDCJWTSignatureAlgorithm {
RS256 = "RS256",
HS256 = "HS256",
RS512 = "RS512"
}
export type TOidcLoginDTO = { export type TOidcLoginDTO = {
externalId: string; externalId: string;
email: string; email: string;
@ -40,6 +46,7 @@ export type TCreateOidcCfgDTO = {
isActive: boolean; isActive: boolean;
orgSlug: string; orgSlug: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
} & TGenericPermission; } & TGenericPermission;
export type TUpdateOidcCfgDTO = Partial<{ export type TUpdateOidcCfgDTO = Partial<{
@ -56,5 +63,6 @@ export type TUpdateOidcCfgDTO = Partial<{
isActive: boolean; isActive: boolean;
orgSlug: string; orgSlug: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
}> & }> &
TGenericPermission; TGenericPermission;

View File

@ -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 AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "Auth0 Client Secret",
type: SecretRotation.Auth0ClientSecret,
connection: AppConnection.Auth0,
template: {
secretsMapping: {
clientId: "AUTH0_CLIENT_ID",
clientSecret: "AUTH0_CLIENT_SECRET"
}
}
};

View File

@ -0,0 +1,104 @@
import {
TAuth0ClientSecretRotationGeneratedCredentials,
TAuth0ClientSecretRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types";
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 { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0";
import { generatePassword } from "../shared/utils";
export const auth0ClientSecretRotationFactory: TRotationFactory<
TAuth0ClientSecretRotationWithConnection,
TAuth0ClientSecretRotationGeneratedCredentials
> = (secretRotation, appConnectionDAL, kmsService) => {
const {
connection,
parameters: { clientId },
secretsMapping
} = secretRotation;
const $rotateClientSecret = async () => {
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
const { audience } = connection.credentials;
await blockLocalAndPrivateIpAddresses(audience);
const clientSecret = generatePassword();
await request.request({
method: "PATCH",
url: `${audience}clients/${clientId}`,
headers: { authorization: `Bearer ${accessToken}` },
data: {
client_secret: clientSecret
}
});
return { clientId, clientSecret };
};
const issueCredentials: TRotationFactoryIssueCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
_,
callback
) => {
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
const { audience } = connection.credentials;
await blockLocalAndPrivateIpAddresses(audience);
// we just trigger an auth0 rotation to negate our credentials
await request.request({
method: "POST",
url: `${audience}clients/${clientId}/rotate-secret`,
headers: { authorization: `Bearer ${accessToken}` }
});
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAuth0ClientSecretRotationGeneratedCredentials> = (
generatedCredentials
) => {
const secrets = [
{
key: secretsMapping.clientId,
value: generatedCredentials.clientId
},
{
key: secretsMapping.clientSecret,
value: generatedCredentials.clientSecret
}
];
return secrets;
};
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@ -0,0 +1,67 @@
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 Auth0ClientSecretRotationGeneratedCredentialsSchema = z
.object({
clientId: z.string(),
clientSecret: z.string()
})
.array()
.min(1)
.max(2);
const Auth0ClientSecretRotationParametersSchema = z.object({
clientId: z
.string()
.trim()
.min(1, "Client ID Required")
.describe(SecretRotations.PARAMETERS.AUTH0_CLIENT_SECRET.clientId)
});
const Auth0ClientSecretRotationSecretsMappingSchema = z.object({
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientId),
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientSecret)
});
export const Auth0ClientSecretRotationTemplateSchema = z.object({
secretsMapping: z.object({
clientId: z.string(),
clientSecret: z.string()
})
});
export const Auth0ClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.Auth0ClientSecret).extend({
type: z.literal(SecretRotation.Auth0ClientSecret),
parameters: Auth0ClientSecretRotationParametersSchema,
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
});
export const CreateAuth0ClientSecretRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.Auth0ClientSecret
).extend({
parameters: Auth0ClientSecretRotationParametersSchema,
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
});
export const UpdateAuth0ClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.Auth0ClientSecret
).extend({
parameters: Auth0ClientSecretRotationParametersSchema.optional(),
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema.optional()
});
export const Auth0ClientSecretRotationListItemSchema = z.object({
name: z.literal("Auth0 Client Secret"),
connection: z.literal(AppConnection.Auth0),
type: z.literal(SecretRotation.Auth0ClientSecret),
template: Auth0ClientSecretRotationTemplateSchema
});

View File

@ -0,0 +1,24 @@
import { z } from "zod";
import { TAuth0Connection } from "@app/services/app-connection/auth0";
import {
Auth0ClientSecretRotationGeneratedCredentialsSchema,
Auth0ClientSecretRotationListItemSchema,
Auth0ClientSecretRotationSchema,
CreateAuth0ClientSecretRotationSchema
} from "./auth0-client-secret-rotation-schemas";
export type TAuth0ClientSecretRotation = z.infer<typeof Auth0ClientSecretRotationSchema>;
export type TAuth0ClientSecretRotationInput = z.infer<typeof CreateAuth0ClientSecretRotationSchema>;
export type TAuth0ClientSecretRotationListItem = z.infer<typeof Auth0ClientSecretRotationListItemSchema>;
export type TAuth0ClientSecretRotationWithConnection = TAuth0ClientSecretRotation & {
connection: TAuth0Connection;
};
export type TAuth0ClientSecretRotationGeneratedCredentials = z.infer<
typeof Auth0ClientSecretRotationGeneratedCredentialsSchema
>;

View File

@ -0,0 +1,3 @@
export * from "./auth0-client-secret-rotation-constants";
export * from "./auth0-client-secret-rotation-schemas";
export * from "./auth0-client-secret-rotation-types";

View File

@ -1,6 +1,7 @@
export enum SecretRotation { export enum SecretRotation {
PostgresCredentials = "postgres-credentials", PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials" MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret"
} }
export enum SecretRotationStatus { export enum SecretRotationStatus {

View File

@ -3,6 +3,7 @@ import { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-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";
@ -16,7 +17,8 @@ import {
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = { const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
}; };
export const listSecretRotationOptions = () => { export const listSecretRotationOptions = () => {

View File

@ -3,10 +3,12 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = { export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials" [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
}; };
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = { export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql [SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
}; };

View File

@ -13,6 +13,7 @@ import {
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { import {
calculateNextRotationAt, calculateNextRotationAt,
@ -41,6 +42,7 @@ import {
TRotationFactory, TRotationFactory,
TSecretRotationRotateGeneratedCredentials, TSecretRotationRotateGeneratedCredentials,
TSecretRotationV2, TSecretRotationV2,
TSecretRotationV2GeneratedCredentials,
TSecretRotationV2Raw, TSecretRotationV2Raw,
TSecretRotationV2WithConnection, TSecretRotationV2WithConnection,
TUpdateSecretRotationV2DTO TUpdateSecretRotationV2DTO
@ -53,6 +55,7 @@ import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { QueueJobs, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, TQueueServiceFactory } from "@app/queue";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns"; import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
@ -97,15 +100,21 @@ export type TSecretRotationV2ServiceFactoryDep = {
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
queueService: Pick<TQueueServiceFactory, "queuePg">; queueService: Pick<TQueueServiceFactory, "queuePg">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
}; };
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>; export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
const MAX_GENERATED_CREDENTIALS_LENGTH = 2; const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactory> = { type TRotationFactoryImplementation = TRotationFactory<
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory, TSecretRotationV2WithConnection,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory TSecretRotationV2GeneratedCredentials
>;
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
}; };
export const secretRotationV2ServiceFactory = ({ export const secretRotationV2ServiceFactory = ({
@ -125,7 +134,8 @@ export const secretRotationV2ServiceFactory = ({
secretQueueService, secretQueueService,
snapshotService, snapshotService,
keyStore, keyStore,
queueService queueService,
appConnectionDAL
}: TSecretRotationV2ServiceFactoryDep) => { }: TSecretRotationV2ServiceFactoryDep) => {
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
const appCfg = getConfig(); const appCfg = getConfig();
@ -429,11 +439,15 @@ export const secretRotationV2ServiceFactory = ({
// validates permission to connect and app is valid for rotation type // validates permission to connect and app is valid for rotation type
const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor); const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor);
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({ const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type](
parameters: payload.parameters, {
secretsMapping, parameters: payload.parameters,
connection secretsMapping,
} as TSecretRotationV2WithConnection); connection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
);
try { try {
const currentTime = new Date(); const currentTime = new Date();
@ -441,7 +455,7 @@ export const secretRotationV2ServiceFactory = ({
// callback structure to support transactional rollback when possible // callback structure to support transactional rollback when possible
const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => { const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => {
const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({ const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({
generatedCredentials: [newCredentials], generatedCredentials: [newCredentials] as TSecretRotationV2GeneratedCredentials,
projectId, projectId,
kmsService kmsService
}); });
@ -740,32 +754,37 @@ export const secretRotationV2ServiceFactory = ({
message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
}); });
const deleteTransaction = secretRotationV2DAL.transaction(async (tx) => { const deleteTransaction = async () =>
if (deleteSecrets) { secretRotationV2DAL.transaction(async (tx) => {
await fnSecretBulkDelete({ if (deleteSecrets) {
secretDAL: secretV2BridgeDAL, await fnSecretBulkDelete({
secretQueueService, secretDAL: secretV2BridgeDAL,
inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({ secretQueueService,
secretKey, inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({
type: SecretType.Shared secretKey,
})), type: SecretType.Shared
projectId, })),
folderId, projectId,
actorId: actor.id, // not actually used since rotated secrets are shared folderId,
tx actorId: actor.id, // not actually used since rotated secrets are shared
}); tx
} });
}
return secretRotationV2DAL.deleteById(rotationId, tx); return secretRotationV2DAL.deleteById(rotationId, tx);
}); });
if (revokeGeneratedCredentials) { if (revokeGeneratedCredentials) {
const appConnection = await decryptAppConnection(connection, kmsService); const appConnection = await decryptAppConnection(connection, kmsService);
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type]({ const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type](
...secretRotation, {
connection: appConnection ...secretRotation,
} as TSecretRotationV2WithConnection); connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
);
const generatedCredentials = await decryptSecretRotationCredentials({ const generatedCredentials = await decryptSecretRotationCredentials({
encryptedGeneratedCredentials, encryptedGeneratedCredentials,
@ -773,9 +792,9 @@ export const secretRotationV2ServiceFactory = ({
kmsService kmsService
}); });
await rotationFactory.revokeCredentials(generatedCredentials, async () => deleteTransaction); await rotationFactory.revokeCredentials(generatedCredentials, deleteTransaction);
} else { } else {
await deleteTransaction; await deleteTransaction();
} }
if (deleteSecrets) { if (deleteSecrets) {
@ -840,10 +859,14 @@ export const secretRotationV2ServiceFactory = ({
const inactiveCredentials = generatedCredentials[inactiveIndex]; const inactiveCredentials = generatedCredentials[inactiveIndex];
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]({ const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation](
...secretRotation, {
connection: appConnection ...secretRotation,
} as TSecretRotationV2WithConnection); connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
);
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => { const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => {
const updatedCredentials = [...generatedCredentials]; const updatedCredentials = [...generatedCredentials];
@ -851,7 +874,7 @@ export const secretRotationV2ServiceFactory = ({
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
projectId, projectId,
generatedCredentials: updatedCredentials, generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
kmsService kmsService
}); });

View File

@ -1,8 +1,17 @@
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import {
TAuth0ClientSecretRotation,
TAuth0ClientSecretRotationGeneratedCredentials,
TAuth0ClientSecretRotationInput,
TAuth0ClientSecretRotationListItem,
TAuth0ClientSecretRotationWithConnection
} from "./auth0-client-secret";
import { import {
TMsSqlCredentialsRotation, TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput, TMsSqlCredentialsRotationInput,
@ -18,17 +27,26 @@ import {
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation } from "./secret-rotation-v2-enums"; import { SecretRotation } from "./secret-rotation-v2-enums";
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation; export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
export type TSecretRotationV2WithConnection = export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection | TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection; | TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials = TSqlCredentialsRotationGeneratedCredentials; export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput; export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput;
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem; export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>; export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
@ -129,27 +147,34 @@ export type TSecretRotationSendNotificationJobPayload = {
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the // transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
// third party credential changes (when supported), preventing credentials getting out of sync // third party credential changes (when supported), preventing credentials getting out of sync
export type TRotationFactoryIssueCredentials = ( export type TRotationFactoryIssueCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw> callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryRevokeCredentials = ( export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
generatedCredentials: TSecretRotationV2GeneratedCredentials, generatedCredentials: T,
callback: () => Promise<TSecretRotationV2Raw> callback: () => Promise<TSecretRotationV2Raw>
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryRotateCredentials = ( export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
credentialsToRevoke: TSecretRotationV2GeneratedCredentials[number] | undefined, credentialsToRevoke: T[number] | undefined,
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw> callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryGetSecretsPayload = ( export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
generatedCredentials: TSecretRotationV2GeneratedCredentials[number] generatedCredentials: T[number]
) => { key: string; value: string }[]; ) => { key: string; value: string }[];
export type TRotationFactory = (secretRotation: TSecretRotationV2WithConnection) => { export type TRotationFactory<
issueCredentials: TRotationFactoryIssueCredentials; T extends TSecretRotationV2WithConnection,
revokeCredentials: TRotationFactoryRevokeCredentials; C extends TSecretRotationV2GeneratedCredentials
rotateCredentials: TRotationFactoryRotateCredentials; > = (
getSecretsPayload: TRotationFactoryGetSecretsPayload; secretRotation: T,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
issueCredentials: TRotationFactoryIssueCredentials<C>;
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
rotateCredentials: TRotationFactoryRotateCredentials<C>;
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;
}; };

View File

@ -1,9 +1,11 @@
import { z } from "zod"; import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema, PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema
]); ]);

View File

@ -1,6 +1,5 @@
import { randomInt } from "crypto";
import { import {
TRotationFactory,
TRotationFactoryGetSecretsPayload, TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials, TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials, TRotationFactoryRevokeCredentials,
@ -8,94 +7,12 @@ import {
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql"; import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
import { generatePassword } from "../utils";
import { import {
TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection TSqlCredentialsRotationWithConnection
} from "./sql-credentials-rotation-types"; } from "./sql-credentials-rotation-types";
const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
const generatePassword = () => {
try {
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};
const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => { const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => {
const error = e as Error; const error = e as Error;
@ -110,7 +27,10 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
return redactedMessage; return redactedMessage;
}; };
export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRotationWithConnection) => { export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials
> = (secretRotation) => {
const { const {
connection, connection,
parameters: { username1, username2 }, parameters: { username1, username2 },
@ -118,7 +38,7 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
secretsMapping secretsMapping
} = secretRotation; } = secretRotation;
const validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
const client = await getSqlConnectionClient({ const client = await getSqlConnectionClient({
...connection, ...connection,
credentials: { credentials: {
@ -136,7 +56,9 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
} }
}; };
const issueCredentials: TRotationFactoryIssueCredentials = async (callback) => { const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
callback
) => {
const client = await getSqlConnectionClient(connection); const client = await getSqlConnectionClient(connection);
// For SQL, since we get existing users, we change both their passwords // For SQL, since we get existing users, we change both their passwords
@ -159,13 +81,16 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
} }
for await (const credentials of credentialsSet) { for await (const credentials of credentialsSet) {
await validateCredentials(credentials); await $validateCredentials(credentials);
} }
return callback(credentialsSet[0]); return callback(credentialsSet[0]);
}; };
const revokeCredentials: TRotationFactoryRevokeCredentials = async (credentialsToRevoke, callback) => { const revokeCredentials: TRotationFactoryRevokeCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
credentialsToRevoke,
callback
) => {
const client = await getSqlConnectionClient(connection); const client = await getSqlConnectionClient(connection);
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
@ -186,7 +111,10 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
return callback(); return callback();
}; };
const rotateCredentials: TRotationFactoryRotateCredentials = async (_, callback) => { const rotateCredentials: TRotationFactoryRotateCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
_,
callback
) => {
const client = await getSqlConnectionClient(connection); const client = await getSqlConnectionClient(connection);
// generate new password for the next active user // generate new password for the next active user
@ -200,12 +128,14 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
await client.destroy(); await client.destroy();
} }
await validateCredentials(credentials); await $validateCredentials(credentials);
return callback(credentials); return callback(credentials);
}; };
const getSecretsPayload: TRotationFactoryGetSecretsPayload = (generatedCredentials) => { const getSecretsPayload: TRotationFactoryGetSecretsPayload<TSqlCredentialsRotationGeneratedCredentials> = (
generatedCredentials
) => {
const { username, password } = secretsMapping; const { username, password } = secretsMapping;
const secrets = [ const secrets = [
@ -226,7 +156,6 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
issueCredentials, issueCredentials,
revokeCredentials, revokeCredentials,
rotateCredentials, rotateCredentials,
getSecretsPayload, getSecretsPayload
validateCredentials
}; };
}; };

View File

@ -0,0 +1,84 @@
import { randomInt } from "crypto";
const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
export const generatePassword = () => {
try {
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};

View File

@ -1782,6 +1782,12 @@ export const AppConnections = {
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.` connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
}), }),
CREDENTIALS: { CREDENTIALS: {
AUTH0_CONNECTION: {
domain: "The domain of the Auth0 instance to connect to.",
clientId: "Your Auth0 application's Client ID.",
clientSecret: "Your Auth0 application's Client Secret.",
audience: "The unique identifier of the target API you want to access."
},
SQL_CONNECTION: { SQL_CONNECTION: {
host: "The hostname of the database server.", host: "The hostname of the database server.",
port: "The port number of the database.", port: "The port number of the database.",
@ -1997,12 +2003,19 @@ export const SecretRotations = {
"The username of the first login to rotate passwords for. This user must already exists in your database.", "The username of the first login to rotate passwords for. This user must already exists in your database.",
username2: username2:
"The username of the second login to rotate passwords for. This user must already exists in your database." "The username of the second login to rotate passwords for. This user must already exists in your database."
},
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
} }
}, },
SECRETS_MAPPING: { SECRETS_MAPPING: {
SQL_CREDENTIALS: { SQL_CREDENTIALS: {
username: "The name of the secret that the active username will be mapped to.", username: "The name of the secret that the active username will be mapped to.",
password: "The name of the secret that the generated password will be mapped to." password: "The name of the secret that the generated password will be mapped to."
},
AUTH0_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."
} }
} }
}; };

View File

@ -1549,7 +1549,8 @@ export const registerRoutes = async (
resourceMetadataDAL, resourceMetadataDAL,
snapshotService, snapshotService,
secretQueueService, secretQueueService,
queueService queueService,
appConnectionDAL
}); });
await secretRotationV2QueueServiceFactory({ await secretRotationV2QueueServiceFactory({

View File

@ -3,6 +3,7 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws"; import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import { import {
AzureAppConfigurationConnectionListItemSchema, AzureAppConfigurationConnectionListItemSchema,
@ -51,7 +52,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedVercelConnectionSchema.options, ...SanitizedVercelConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options, ...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options, ...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options ...SanitizedCamundaConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -66,7 +68,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
VercelConnectionListItemSchema, VercelConnectionListItemSchema,
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema, MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema CamundaConnectionListItemSchema,
Auth0ConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@ -0,0 +1,51 @@
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 {
CreateAuth0ConnectionSchema,
SanitizedAuth0ConnectionSchema,
UpdateAuth0ConnectionSchema
} from "@app/services/app-connection/auth0";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerAuth0ConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Auth0,
server,
sanitizedResponseSchema: SanitizedAuth0ConnectionSchema,
createSchema: CreateAuth0ConnectionSchema,
updateSchema: UpdateAuth0ConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/clients`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
clients: z.object({ name: z.string(), id: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const clients = await server.services.appConnection.auth0.listClients(connectionId, req.permission);
return { clients };
}
});
};

View File

@ -1,3 +1,4 @@
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router";
@ -28,5 +29,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Vercel]: registerVercelConnectionRouter, [AppConnection.Vercel]: registerVercelConnectionRouter,
[AppConnection.Postgres]: registerPostgresConnectionRouter, [AppConnection.Postgres]: registerPostgresConnectionRouter,
[AppConnection.MsSql]: registerMsSqlConnectionRouter, [AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter [AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter
}; };

View File

@ -10,7 +10,8 @@ export enum AppConnection {
Vercel = "vercel", Vercel = "vercel",
Postgres = "postgres", Postgres = "postgres",
MsSql = "mssql", MsSql = "mssql",
Camunda = "camunda" Camunda = "camunda",
Auth0 = "auth0"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@ -16,6 +16,7 @@ import {
TAppConnectionCredentialsValidator, TAppConnectionCredentialsValidator,
TAppConnectionTransitionCredentialsToPlatform TAppConnectionTransitionCredentialsToPlatform
} from "./app-connection-types"; } from "./app-connection-types";
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws"; import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
import { import {
AzureAppConfigurationConnectionMethod, AzureAppConfigurationConnectionMethod,
@ -63,7 +64,8 @@ export const listAppConnectionOptions = () => {
getVercelConnectionListItem(), getVercelConnectionListItem(),
getPostgresConnectionListItem(), getPostgresConnectionListItem(),
getMsSqlConnectionListItem(), getMsSqlConnectionListItem(),
getCamundaConnectionListItem() getCamundaConnectionListItem(),
getAuth0ConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@ -109,25 +111,28 @@ export const decryptAppConnectionCredentials = async ({
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
}; };
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureAppConfiguration]:
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
};
export const validateAppConnectionCredentials = async ( export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig appConnection: TAppConnectionConfig
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); ): Promise<TAppConnection["credentials"]> => {
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureAppConfiguration]:
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
};
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
switch (method) { switch (method) {
@ -154,6 +159,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
return "Username & Password"; return "Username & Password";
case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials";
default: default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`); throw new Error(`Unhandled App Connection Method: ${method}`);
@ -196,5 +203,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported, [AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
[AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported [AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
}; };

View File

@ -12,5 +12,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Vercel]: "Vercel", [AppConnection.Vercel]: "Vercel",
[AppConnection.Postgres]: "PostgreSQL", [AppConnection.Postgres]: "PostgreSQL",
[AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.MsSql]: "Microsoft SQL Server",
[AppConnection.Camunda]: "Camunda" [AppConnection.Camunda]: "Camunda",
[AppConnection.Auth0]: "Auth0"
}; };

View File

@ -14,6 +14,7 @@ import {
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
validateAppConnectionCredentials validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns"; } from "@app/services/app-connection/app-connection-fns";
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal"; import { TAppConnectionDALFactory } from "./app-connection-dal";
@ -27,6 +28,7 @@ import {
TUpdateAppConnectionDTO, TUpdateAppConnectionDTO,
TValidateAppConnectionCredentialsSchema TValidateAppConnectionCredentialsSchema
} from "./app-connection-types"; } from "./app-connection-types";
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service"; import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
@ -68,7 +70,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema, [AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema, [AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema, [AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema [AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@ -442,6 +445,7 @@ export const appConnectionServiceFactory = ({
humanitec: humanitecConnectionService(connectAppConnectionById), humanitec: humanitecConnectionService(connectAppConnectionById),
terraformCloud: terraformCloudConnectionService(connectAppConnectionById), terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById) vercel: vercelConnectionService(connectAppConnectionById),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
}; };
}; };

View File

@ -3,6 +3,12 @@ import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sq
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { AWSRegion } from "./app-connection-enums"; import { AWSRegion } from "./app-connection-enums";
import {
TAuth0Connection,
TAuth0ConnectionConfig,
TAuth0ConnectionInput,
TValidateAuth0ConnectionCredentialsSchema
} from "./auth0";
import { import {
TAwsConnection, TAwsConnection,
TAwsConnectionConfig, TAwsConnectionConfig,
@ -83,6 +89,7 @@ export type TAppConnection = { id: string } & (
| TPostgresConnection | TPostgresConnection
| TMsSqlConnection | TMsSqlConnection
| TCamundaConnection | TCamundaConnection
| TAuth0Connection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@ -102,6 +109,7 @@ export type TAppConnectionInput = { id: string } & (
| TPostgresConnectionInput | TPostgresConnectionInput
| TMsSqlConnectionInput | TMsSqlConnectionInput
| TCamundaConnectionInput | TCamundaConnectionInput
| TAuth0ConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@ -124,9 +132,10 @@ export type TAppConnectionConfig =
| TDatabricksConnectionConfig | TDatabricksConnectionConfig
| THumanitecConnectionConfig | THumanitecConnectionConfig
| TTerraformCloudConnectionConfig | TTerraformCloudConnectionConfig
| TVercelConnectionConfig
| TSqlConnectionConfig | TSqlConnectionConfig
| TCamundaConnectionConfig; | TCamundaConnectionConfig
| TVercelConnectionConfig
| TAuth0ConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@ -139,8 +148,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidatePostgresConnectionCredentialsSchema | TValidatePostgresConnectionCredentialsSchema
| TValidateMsSqlConnectionCredentialsSchema | TValidateMsSqlConnectionCredentialsSchema
| TValidateCamundaConnectionCredentialsSchema | TValidateCamundaConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema | TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema; | TValidateAuth0ConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@ -0,0 +1,3 @@
export enum Auth0ConnectionMethod {
ClientCredentials = "client-credentials"
}

View File

@ -0,0 +1,97 @@
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
import { TAuth0AccessTokenResponse, TAuth0Connection, TAuth0ConnectionConfig } from "./auth0-connection-types";
export const getAuth0ConnectionListItem = () => {
return {
name: "Auth0" as const,
app: AppConnection.Auth0 as const,
methods: Object.values(Auth0ConnectionMethod) as [Auth0ConnectionMethod.ClientCredentials]
};
};
const authorizeAuth0Connection = async ({
clientId,
clientSecret,
domain,
audience
}: TAuth0ConnectionConfig["credentials"]) => {
const instanceUrl = domain.startsWith("http") ? domain : `https://${domain}`;
await blockLocalAndPrivateIpAddresses(instanceUrl);
const { data } = await request.request<TAuth0AccessTokenResponse>({
method: "POST",
url: `${removeTrailingSlash(instanceUrl)}/oauth/token`,
headers: { "content-type": "application/x-www-form-urlencoded" },
data: new URLSearchParams({
grant_type: "client_credentials", // this will need to be resolved if we support methods other than client credentials
client_id: clientId,
client_secret: clientSecret,
audience
})
});
if (data.token_type !== "Bearer") {
throw new Error(`Unhandled token type: ${data.token_type}`);
}
return {
accessToken: data.access_token,
// cap token lifespan to 10 minutes
expiresAt: Math.min(data.expires_in * 1000, 600000) + Date.now()
};
};
export const getAuth0ConnectionAccessToken = async (
{ id, orgId, credentials }: TAuth0Connection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const { expiresAt, accessToken } = credentials;
// get new token if expired or less than 5 minutes until expiry
if (Date.now() < expiresAt - 300000) {
return accessToken;
}
const authData = await authorizeAuth0Connection(credentials);
const updatedCredentials: TAuth0Connection["credentials"] = {
...credentials,
...authData
};
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
});
await appConnectionDAL.updateById(id, { encryptedCredentials });
return authData.accessToken;
};
export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0ConnectionConfig) => {
try {
const { accessToken, expiresAt } = await authorizeAuth0Connection(credentials);
return {
...credentials,
accessToken,
expiresAt
};
} catch (e: unknown) {
throw new BadRequestError({
message: (e as Error).message ?? `Unable to validate connection: verify credentials`
});
}
};

View File

@ -0,0 +1,94 @@
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 { Auth0ConnectionMethod } from "./auth0-connection-enums";
export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({
domain: z.string().trim().min(1, "Domain required").describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.domain),
clientId: z
.string()
.trim()
.min(1, "Client ID required")
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientId),
clientSecret: z
.string()
.trim()
.min(1, "Client Secret required")
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientSecret),
audience: z
.string()
.trim()
.url()
.min(1, "Audience required")
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.audience)
});
const Auth0ConnectionClientCredentialsOutputCredentialsSchema = z
.object({
accessToken: z.string(),
expiresAt: z.number()
})
.merge(Auth0ConnectionClientCredentialsInputCredentialsSchema);
const BaseAuth0ConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Auth0)
});
export const Auth0ConnectionSchema = z.intersection(
BaseAuth0ConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
credentials: Auth0ConnectionClientCredentialsOutputCredentialsSchema
})
])
);
export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [
BaseAuth0ConnectionSchema.extend({
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.pick({
domain: true,
clientId: true,
audience: true
})
})
]);
export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(Auth0ConnectionMethod.ClientCredentials)
.describe(AppConnections.CREATE(AppConnection.Auth0).method),
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Auth0).credentials
)
})
]);
export const CreateAuth0ConnectionSchema = ValidateAuth0ConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Auth0)
);
export const UpdateAuth0ConnectionSchema = z
.object({
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Auth0).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0));
export const Auth0ConnectionListItemSchema = z.object({
name: z.literal("Auth0"),
app: z.literal(AppConnection.Auth0),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(Auth0ConnectionMethod).array()
});

View File

@ -0,0 +1,71 @@
import { request } from "@app/lib/config/request";
import { OrgServiceActor } from "@app/lib/types";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0/auth0-connection-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAuth0Connection, TAuth0ListClient, TAuth0ListClientsResponse } from "./auth0-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TAuth0Connection>;
const listAuth0Clients = async (
appConnection: TAuth0Connection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const accessToken = await getAuth0ConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
const { audience, clientId: connectionClientId } = appConnection.credentials;
await blockLocalAndPrivateIpAddresses(audience);
const clients: TAuth0ListClient[] = [];
let hasMore = true;
let page = 0;
while (hasMore) {
// eslint-disable-next-line no-await-in-loop
const { data: clientsPage } = await request.get<TAuth0ListClientsResponse>(`${audience}clients`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
},
params: {
include_totals: true,
per_page: 100,
page
}
});
clients.push(...clientsPage.clients);
page += 1;
hasMore = clientsPage.total > clients.length;
}
return (
clients.filter((client) => client.client_id !== connectionClientId && client.name !== "All Applications") ?? []
);
};
export const auth0ConnectionService = (
getAppConnection: TGetAppConnectionFunc,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const listClients = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Auth0, connectionId, actor);
const clients = await listAuth0Clients(appConnection, appConnectionDAL, kmsService);
return clients.map((client) => ({ id: client.client_id, name: client.name }));
};
return {
listClients
};
};

View File

@ -0,0 +1,39 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
Auth0ConnectionSchema,
CreateAuth0ConnectionSchema,
ValidateAuth0ConnectionCredentialsSchema
} from "./auth0-connection-schemas";
export type TAuth0Connection = z.infer<typeof Auth0ConnectionSchema>;
export type TAuth0ConnectionInput = z.infer<typeof CreateAuth0ConnectionSchema> & {
app: AppConnection.Auth0;
};
export type TValidateAuth0ConnectionCredentialsSchema = typeof ValidateAuth0ConnectionCredentialsSchema;
export type TAuth0ConnectionConfig = DiscriminativePick<TAuth0Connection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TAuth0AccessTokenResponse = {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
};
export type TAuth0ListClient = {
name: string;
client_id: string;
};
export type TAuth0ListClientsResponse = {
total: number;
clients: TAuth0ListClient[];
};

View File

@ -0,0 +1,4 @@
export * from "./auth0-connection-enums";
export * from "./auth0-connection-fns";
export * from "./auth0-connection-schemas";
export * from "./auth0-connection-types";

View File

@ -1,6 +1,7 @@
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
@ -26,6 +27,8 @@ const authorizeDatabricksConnection = async ({
clientSecret, clientSecret,
workspaceUrl workspaceUrl
}: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => { }: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
await blockLocalAndPrivateIpAddresses(workspaceUrl);
const { data } = await request.post<TAuthorizeDatabricksConnection>( const { data } = await request.post<TAuthorizeDatabricksConnection>(
`${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`, `${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`,
"grant_type=client_credentials&scope=all-apis", "grant_type=client_credentials&scope=all-apis",

View File

@ -16,7 +16,7 @@ export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.obje
workspaceUrl: z.string().trim().url().min(1, "Workspace URL required") workspaceUrl: z.string().trim().url().min(1, "Workspace URL required")
}); });
export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
.object({ .object({
accessToken: z.string(), accessToken: z.string(),
expiresAt: z.number() expiresAt: z.number()

View File

@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/auth0/available"
---

View File

@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/auth0"
---
<Note>
Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the
required credentials.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/auth0/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/auth0/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/auth0/connection-name/{connectionName}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/auth0"
---

View File

@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/auth0/{connectionId}"
---
<Note>
Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the
required credentials.
</Note>

View File

@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/auth0-client-secret"
---
<Note>
Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the
required parameters.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/auth0-client-secret/{rotationId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret/rotation-name/{rotationName}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}/generated-credentials"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret"
---

View File

@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/auth0-client-secret/{rotationId}/rotate-secrets"
---

View File

@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/auth0-client-secret/{rotationId}"
---
<Note>
Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the
required parameters.
</Note>

View File

@ -5,6 +5,6 @@ openapi: "POST /api/v2/secret-rotations/mssql-credentials"
<Note> <Note>
Check out the configuration docs for [Microsoft SQL Server Check out the configuration docs for [Microsoft SQL Server
Credentials Rotations](/documentation/platform/secret-rotation/mssql) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

View File

@ -5,6 +5,6 @@ openapi: "PATCH /api/v2/secret-rotations/mssql-credentials/{rotationId}"
<Note> <Note>
Check out the configuration docs for [Microsoft SQL Server Check out the configuration docs for [Microsoft SQL Server
Credentials Rotations](/documentation/platform/secret-rotation/mssql) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

View File

@ -5,6 +5,6 @@ openapi: "POST /api/v2/secret-rotations/postgres-credentials"
<Note> <Note>
Check out the configuration docs for [PostgreSQL Check out the configuration docs for [PostgreSQL
Credentials Rotations](/documentation/platform/secret-rotation/postgres) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

View File

@ -5,6 +5,6 @@ openapi: "PATCH /api/v2/secret-rotations/postgres-credentials/{rotationId}"
<Note> <Note>
Check out the configuration docs for [PostgreSQL Check out the configuration docs for [PostgreSQL
Credentials Rotations](/documentation/platform/secret-rotation/postgres) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

View File

@ -173,7 +173,7 @@ The changelog below reflects new product developments and updates on a monthly b
- Replaced internal [Winston](https://github.com/winstonjs/winston) with [Pino](https://github.com/pinojs/pino) logging library with external logging to AWS CloudWatch - Replaced internal [Winston](https://github.com/winstonjs/winston) with [Pino](https://github.com/pinojs/pino) logging library with external logging to AWS CloudWatch
- Added admin panel to self-hosting experience. - Added admin panel to self-hosting experience.
- Released [secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview) feature with preliminary support for rotating [SendGrid](https://infisical.com/docs/documentation/platform/secret-rotation/sendgrid), [PostgreSQL/CockroachDB](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), and [MySQL/MariaDB](https://infisical.com/docs/documentation/platform/secret-rotation/mysql) credentials. - Released [secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview) feature with preliminary support for rotating [SendGrid](https://infisical.com/docs/documentation/platform/secret-rotation/sendgrid), [PostgreSQL/CockroachDB](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), and [MySQL/MariaDB](https://infisical.com/docs/documentation/platform/secret-rotation/mysql) credentials.
- Released secret reminders feature. - Released secret reminders feature.
## Oct 2023 ## Oct 2023

View File

@ -0,0 +1,154 @@
---
title: "Auth0 Client Secret"
description: "Learn how to automatically rotate Auth0 Client Secrets."
---
<Note>
Due to how Auth0 client secrets are rotated, retired credentials will not be able to
authenticate with Auth0 during their [inactive period](./overview#how-rotation-works).
This is a limitation of the Auth0 platform and cannot be
rectified by Infisical.
</Note>
## Prerequisites
- Create an [Auth0 Connection](/integrations/app-connections/auth0) with the required **Secret Rotation** audience and permissions
## Create an Auth0 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 **Auth0 Client Secret** option.
![Select Auth0 Client Secret](/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png)
3. Select the **Auth0 Connection** to use and configure the rotation behavior. Then click **Next**.
![Rotation Configuration](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png)
- **Auth0 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.
<Note>
Due to Auth0 Client Secret Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option.
</Note>
4. Select the Auth0 application whose Client Secret you want to rotate. Then click **Next**.
![Rotation Parameters](/images/secret-rotations-v2/auth0-client-secret/auth0-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/auth0-client-secret/auth0-client-secret-secrets-mapping.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/auth0-client-secret/auth0-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/auth0-client-secret/auth0-client-secret-confirm.png)
8. Your **Auth0 Client Secret** credentials are now available for use via the mapped secrets.
![Rotation Created](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png)
</Tab>
<Tab title="API">
To create an Auth0 Client Secret Rotation, make an API request to the [Create Auth0
Client Secret Rotation](/api-reference/endpoints/secret-rotations/auth0-client-secret/create) API endpoint.
You will first need the **Client ID** of the Auth0 application you want to rotate the secret for. This can be obtained from the Applications dashboard.
![Auth0 Client ID](/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png)
### Sample request
```bash Request
curl --request POST \
--url https://us.infisical.com/api/v2/secret-rotations/auth0-client-secret \
--header 'Content-Type: application/json' \
--data '{
"name": "my-auth0-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": "AUTH0_CLIENT_ID",
"clientSecret": "AUTH0_CLIENT_SECRET"
}
}'
```
<Note>
Due to Auth0 Client Secret Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option.
</Note>
### Sample response
```bash Response
{
"secretRotation": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-auth0-rotation",
"description": "my client secret rotation",
"secretsMapping": {
"clientId": "AUTH0_CLIENT_ID",
"clientSecret": "AUTH0_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": "auth0",
"name": "my-auth0-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": "auth0-client-secret",
"parameters": {
"clientId": "...",
}
}
}
```
</Tab>
</Tabs>

View File

@ -47,6 +47,11 @@ Each set of credentials transitions through three distinct states:
- **Active**: The primary credentials that will be used for new connections - **Active**: The primary credentials that will be used for new connections
- **Inactive**: These credentials are still valid but are no longer issued for new connections - **Inactive**: These credentials are still valid but are no longer issued for new connections
<Note>
Some rotation providers utilize a single credential set due to technical constraints. As a result, inactive credentials for these providers will immediately become invalid once rotated.
To avoid service interruptions, Infisical recommends manually rotating these credentials to prevent downtime.
</Note>
- **Revoked**: Permanently invalidated and deleted from the system - **Revoked**: Permanently invalidated and deleted from the system
### Rotation Cycle Example (30-Day Interval) ### Rotation Cycle Example (30-Day Interval)
@ -95,3 +100,16 @@ Using a __30-Day__ rotation interval as an example, here's how the process unfol
- [PostgreSQL Credentials](./postgres) - [PostgreSQL Credentials](./postgres)
- [Microsoft SQL Server Credentials](./mssql) - [Microsoft SQL Server Credentials](./mssql)
## FAQ
<AccordionGroup>
<Accordion title="Why do certain rotations only use a single credential set?">
Some credential providers have limitations that affect rotation patterns:
- The third-party provider's API only supports managing one active credential set at a time
- The specific use-case (such as personal login accounts) is inherently limited to a single active credential
In either scenario, when service continuity is critical, Infisical recommends disabling auto-rotation and performing manual credential rotation during scheduled maintenance windows.
</Accordion>
</AccordionGroup>

View File

@ -42,7 +42,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**. 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**.
![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png)
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png)
Once you've done that, press **Update** to complete the required configuration. Once you've done that, press **Update** to complete the required configuration.

View File

@ -69,7 +69,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png) ![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png)
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**.
![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png) ![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png)
Once you've done that, press **Update** to complete the required configuration. Once you've done that, press **Update** to complete the required configuration.

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 KiB

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 588 KiB

View File

@ -0,0 +1,101 @@
---
title: "Auth0 Connection"
description: "Learn how to configure an Auth0 Connection for Infisical."
---
Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow) to connect with your Auth0 applications.
## Configure a Machine-to-Machine Application in Auth0
<Steps>
<Step title="Auth0 Applications Dashboard">
Navigate to the **Applications** page in Auth0 via the sidebar and click **Create Application**.
![Applications Page](/images/app-connections/auth0/auth0-dashboard-applications.png)
</Step>
<Step title="Create a Machine-to-Machine Application">
Give your application a name and select **Machine-to-Machine** for the application type.
![Create Machine-to-Machine Application](/images/app-connections/auth0/auth0-select-m2m.png)
</Step>
<Step title="Configure Authorization">
Depending on your connection use case, authorize your application for the applicable API and grant the relevant permissions. Once done, click **Authorize**.
<Tabs>
<Tab title="Secret Rotation">
Select the **Auth0 Management API** option from the dropdown and grant the `update:client_keys` and `read:clients` permission.
![Secret Rotation Authorization](/images/app-connections/auth0/auth0-secret-rotation-api-selection.png)
</Tab>
</Tabs>
</Step>
<Step title="Application Client Credentials">
On your application page, select the **Settings** tab and copy the **Domain**, **Client ID** and **Client Secret** for later.
![Client Credentials](/images/app-connections/auth0/auth0-client-credentials.png)
</Step>
<Step title="Application Audience">
Next, select the **APIs** tab and copy the **API Identifier**.
![Application Audience](/images/app-connections/auth0/auth0-audience.png)
</Step>
</Steps>
## Setup Auth0 Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
1. Navigate to the App Connections tab on the Organization Settings page.
![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **Auth0 Connection** option.
![Select Auth0 Connection](/images/app-connections/auth0/select-auth0-connection.png)
3. Select the **Client Credentials** method option and provide the details obtained from the previous section and press **Connect to Auth0**.
![Create Auth0 Connection](/images/app-connections/auth0/client-credentials-create.png)
4. Your **Auth0 Connection** is now available for use.
![Assume Role Auth0 Connection](/images/app-connections/auth0/client_credentials_connection.png)
</Tab>
<Tab title="API">
To create a Auth0 Connection, make an API request to the [Create Auth0
Connection](/api-reference/endpoints/app-connections/auth0/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/auth0 \
--header 'Content-Type: application/json' \
--data '{
"name": "my-auth0-connection",
"method": "client-credentials",
"credentials": {
"domain": "xxx-xxxxxxxxx.us.auth0.com",
"clientId": "...",
"clientSecret": "...",
"audience": "https://xxx-xxxxxxxxx.us.auth0.com/api/v2/"
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-auth0-connection",
"version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"app": "auth0",
"method": "client-credentials",
"credentials": {
"domain": "xxx-xxxxxxxxx.us.auth0.com",
"clientId": "...",
"audience": "https://xxx-xxxxxxxxx.us.auth0.com/api/v2/"
}
}
}
```
</Tab>
</Tabs>

View File

@ -178,8 +178,9 @@
"group": "Secret Rotation", "group": "Secret Rotation",
"pages": [ "pages": [
"documentation/platform/secret-rotation/overview", "documentation/platform/secret-rotation/overview",
"documentation/platform/secret-rotation/postgres", "documentation/platform/secret-rotation/auth0-client-secret",
"documentation/platform/secret-rotation/mssql" "documentation/platform/secret-rotation/postgres-credentials",
"documentation/platform/secret-rotation/mssql-credentials"
] ]
}, },
{ {
@ -414,6 +415,7 @@
{ {
"group": "Connections", "group": "Connections",
"pages": [ "pages": [
"integrations/app-connections/auth0",
"integrations/app-connections/aws", "integrations/app-connections/aws",
"integrations/app-connections/azure-app-configuration", "integrations/app-connections/azure-app-configuration",
"integrations/app-connections/azure-key-vault", "integrations/app-connections/azure-key-vault",
@ -844,6 +846,19 @@
"pages": [ "pages": [
"api-reference/endpoints/secret-rotations/list", "api-reference/endpoints/secret-rotations/list",
"api-reference/endpoints/secret-rotations/options", "api-reference/endpoints/secret-rotations/options",
{
"group": "Auth0 Client Secret",
"pages": [
"api-reference/endpoints/secret-rotations/auth0-client-secret/create",
"api-reference/endpoints/secret-rotations/auth0-client-secret/delete",
"api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id",
"api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name",
"api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id",
"api-reference/endpoints/secret-rotations/auth0-client-secret/list",
"api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets",
"api-reference/endpoints/secret-rotations/auth0-client-secret/update"
]
},
{ {
"group": "Microsoft SQL Server Credentials", "group": "Microsoft SQL Server Credentials",
"pages": [ "pages": [
@ -888,6 +903,18 @@
"pages": [ "pages": [
"api-reference/endpoints/app-connections/list", "api-reference/endpoints/app-connections/list",
"api-reference/endpoints/app-connections/options", "api-reference/endpoints/app-connections/options",
{
"group": "Auth0",
"pages": [
"api-reference/endpoints/app-connections/auth0/list",
"api-reference/endpoints/app-connections/auth0/available",
"api-reference/endpoints/app-connections/auth0/get-by-id",
"api-reference/endpoints/app-connections/auth0/get-by-name",
"api-reference/endpoints/app-connections/auth0/create",
"api-reference/endpoints/app-connections/auth0/update",
"api-reference/endpoints/app-connections/auth0/delete"
]
},
{ {
"group": "AWS", "group": "AWS",
"pages": [ "pages": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -1,4 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms"; import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms";
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader"; import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
@ -54,7 +56,24 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
selectedRotation ? ( selectedRotation ? (
<SecretRotationV2ModalHeader isConfigured={false} type={selectedRotation} /> <SecretRotationV2ModalHeader isConfigured={false} type={selectedRotation} />
) : ( ) : (
"Add Secret Rotation" <div className="flex items-center text-mineshaft-300">
Add Secret Rotation
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
) )
} }
onPointerDownOutside={(e) => e.preventDefault()} onPointerDownOutside={(e) => e.preventDefault()}

View File

@ -24,7 +24,7 @@ export const SecretRotationV2ModalHeader = ({ type, isConfigured }: Props) => {
{destinationDetails.name} Rotation {destinationDetails.name} Rotation
<a <a
target="_blank" target="_blank"
href={`https://infisical.com/docs/platform/secret-rotations/${type}`} href={`https://infisical.com/docs/documentation/platform/secret-rotation/${type}`}
className="mb-1 ml-1" className="mb-1 ml-1"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@ -24,17 +24,7 @@ export const SecretRotationV2Select = ({ onSelect }: Props) => {
return ( return (
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{secretRotationOptions?.map(({ type }) => { {secretRotationOptions?.map(({ type }) => {
const { image, name } = SECRET_ROTATION_MAP[type]; const { image, name, size } = SECRET_ROTATION_MAP[type];
let size: number;
switch (type) {
case SecretRotation.MsSqlCredentials:
size = 50;
break;
default:
size = 45;
}
return ( return (
<button <button

View File

@ -0,0 +1,56 @@
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { TAuth0ClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation";
import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
type Props = {
generatedCredentialsResponse: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
};
export const ViewAuth0ClientSecretRotationGeneratedCredentials = ({
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>
</>
}
/>
<NoticeBannerV2 title="Auth0 Retired Credentials Behavior">
<p className="text-sm text-mineshaft-300">
Due to how Auth0 client secrets are rotated, retired credentials will not be able to
authenticate with Auth0 during their{" "}
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview#how-rotation-works"
rel="noopener noreferrer"
className="underline decoration-primary underline-offset-2 hover:text-mineshaft-200"
>
inactive period
</a>
. This is a limitation of the Auth0 platform and cannot be rectified by Infisical.
</p>
</NoticeBannerV2>
</>
);
};

View File

@ -3,6 +3,7 @@ import { faRotate } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
import { ViewAuth0ClientSecretRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials";
import { Modal, ModalContent, Spinner } from "@app/components/v2"; import { Modal, ModalContent, Spinner } from "@app/components/v2";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { import {
@ -11,7 +12,7 @@ import {
useViewSecretRotationV2GeneratedCredentials useViewSecretRotationV2GeneratedCredentials
} from "@app/hooks/api/secretRotationsV2"; } from "@app/hooks/api/secretRotationsV2";
import { ViewSqlRotationGeneratedCredentials } from "./shared"; import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared";
type Props = { type Props = {
secretRotation?: TSecretRotationV2; secretRotation?: TSecretRotationV2;
@ -54,7 +55,14 @@ const Content = ({ secretRotation }: ContentProps) => {
case SecretRotation.PostgresCredentials: case SecretRotation.PostgresCredentials:
case SecretRotation.MsSqlCredentials: case SecretRotation.MsSqlCredentials:
Component = ( Component = (
<ViewSqlRotationGeneratedCredentials <ViewSqlCredentialsRotationGeneratedCredentials
generatedCredentialsResponse={generatedCredentialsResponse}
/>
);
break;
case SecretRotation.Auth0ClientSecret:
Component = (
<ViewAuth0ClientSecretRotationGeneratedCredentials
generatedCredentialsResponse={generatedCredentialsResponse} generatedCredentialsResponse={generatedCredentialsResponse}
/> />
); );

View File

@ -0,0 +1,48 @@
import { ReactNode } from "react";
import { faCheck, faClockRotateLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type Props = {
activeCredentials: ReactNode;
inactiveCredentials?: ReactNode;
};
export const ViewRotationGeneratedCredentialsDisplay = ({
activeCredentials,
inactiveCredentials
}: Props) => {
return (
<>
<div className="flex flex-col gap-2">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-100">
<FontAwesomeIcon icon={faCheck} className="mr-1.5 text-green" />
Current Credentials
</span>
</div>
<p className="text-sm text-mineshaft-300">
The active credential set currently mapped to the rotation secrets.
</p>
<div className="flex flex-col gap-x-8 gap-y-2 rounded border border-mineshaft-600 bg-mineshaft-700 p-2">
{activeCredentials}
</div>
</div>
{inactiveCredentials && (
<div className="flex flex-col gap-2">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-100">
<FontAwesomeIcon icon={faClockRotateLeft} className="mr-1.5 text-yellow" />
Retired Credentials
</span>
</div>
<p className="text-sm text-mineshaft-300">
The retired credential set that will be revoked during the next rotation cycle.
</p>
<div className="flex flex-col gap-x-8 gap-y-2 rounded border border-mineshaft-600 bg-mineshaft-700 p-2">
{inactiveCredentials}
</div>
</div>
)}
</>
);
};

View File

@ -0,0 +1,40 @@
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
import { ViewRotationGeneratedCredentialsDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/ViewRotationGeneratedCredentialsDisplay";
import { TMsSqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mssql-credentials-rotation";
import { TPostgresCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/postgres-credentials-rotation";
type Props = {
generatedCredentialsResponse:
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
| TPostgresCredentialsRotationGeneratedCredentialsResponse;
};
export const ViewSqlCredentialsRotationGeneratedCredentials = ({
generatedCredentialsResponse: { generatedCredentials, activeIndex }
}: Props) => {
const inactiveIndex = activeIndex === 0 ? 1 : 0;
const activeCredentials = generatedCredentials[activeIndex];
const inactiveCredentials = generatedCredentials[inactiveIndex];
return (
<ViewRotationGeneratedCredentialsDisplay
activeCredentials={
<>
<CredentialDisplay label="Username">{activeCredentials?.username}</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{activeCredentials?.password}
</CredentialDisplay>
</>
}
inactiveCredentials={
<>
<CredentialDisplay label="Username">{inactiveCredentials?.username}</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{inactiveCredentials?.password}
</CredentialDisplay>
</>
}
/>
);
};

View File

@ -1,57 +0,0 @@
import { faCheck, faClockRotateLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
import { TViewSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2";
type Props = {
generatedCredentialsResponse: TViewSecretRotationGeneratedCredentialsResponse;
};
export const ViewSqlRotationGeneratedCredentials = ({
generatedCredentialsResponse: { generatedCredentials, activeIndex }
}: Props) => {
const inactiveIndex = activeIndex === 0 ? 1 : 0;
const activeCredentials = generatedCredentials[activeIndex];
const inactiveCredentials = generatedCredentials[inactiveIndex];
return (
<>
<div className="flex flex-col gap-2">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-100">
<FontAwesomeIcon icon={faCheck} className="mr-1.5 text-green" />
Current Credentials
</span>
</div>
<p className="text-sm text-mineshaft-300">
The active credential set currently mapped to the rotation secrets.
</p>
<div className="flex flex-col gap-x-8 gap-y-2 rounded border border-mineshaft-600 bg-mineshaft-700 p-2">
<CredentialDisplay label="Username">{activeCredentials?.username}</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{activeCredentials?.password}
</CredentialDisplay>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-100">
<FontAwesomeIcon icon={faClockRotateLeft} className="mr-1.5 text-yellow" />
Retired Credentials
</span>
</div>
<p className="text-sm text-mineshaft-300">
The retired credential set that will be revoked during the next rotation cycle.
</p>
<div className="flex flex-col gap-x-8 gap-y-2 rounded border border-mineshaft-600 bg-mineshaft-700 p-2">
<CredentialDisplay label="Username">{inactiveCredentials?.username}</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{inactiveCredentials?.password}
</CredentialDisplay>
</div>
</div>
</>
);
};

View File

@ -1 +1,3 @@
export * from "./ViewSqlRotationGeneratedCredentials"; export * from "./CredentialDisplay";
export * from "./ViewRotationGeneratedCredentialsDisplay";
export * from "./ViewSqlCredentialsRotationGeneratedCredentials";

View File

@ -1,8 +1,14 @@
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, setHours, setMinutes } from "date-fns"; import { format, setHours, setMinutes } from "date-fns";
import { FilterableSelect, FormControl, Input, Switch } from "@app/components/v2"; import { FilterableSelect, FormControl, Input, Switch } from "@app/components/v2";
import { getRotateAtLocal } from "@app/helpers/secretRotationsV2"; import {
getRotateAtLocal,
IS_ROTATION_DUAL_CREDENTIALS,
SECRET_ROTATION_MAP
} from "@app/helpers/secretRotationsV2";
import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
import { TSecretRotationV2Form } from "./schemas"; import { TSecretRotationV2Form } from "./schemas";
@ -14,7 +20,9 @@ type Props = {
}; };
export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => { export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => {
const { control } = useFormContext<TSecretRotationV2Form>(); const { control, watch } = useFormContext<TSecretRotationV2Form>();
const [type, isAutoRotationEnabled] = watch(["type", "isAutoRotationEnabled"]);
return ( return (
<> <>
@ -117,6 +125,23 @@ export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }:
); );
}} }}
/> />
{!IS_ROTATION_DUAL_CREDENTIALS[type] && isAutoRotationEnabled && (
<div className="rounded border border-yellow bg-yellow/10 p-2 px-3 text-xs text-yellow">
<FontAwesomeIcon icon={faWarning} className="mr-1" /> Due to{" "}
{SECRET_ROTATION_MAP[type].name} Rotations rotating a single credential set, auto-rotation
may result in service interruptions. If you need to ensure service continuity, we
recommend disabling this option. <br />
<a
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview#how-rotation-works"
target="_blank"
rel="noopener noreferrer"
className="underline decoration-yellow underline-offset-2 hover:text-mineshaft-200"
>
Read more
</a>
.
</div>
)}
</> </>
); );
}; };

View File

@ -12,7 +12,7 @@ import { SecretRotationV2ReviewFields } from "@app/components/secret-rotations-v
import { SecretRotationV2SecretsMappingFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields"; import { SecretRotationV2SecretsMappingFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; import { IS_ROTATION_DUAL_CREDENTIALS, SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { import {
SecretRotation, SecretRotation,
TSecretRotationV2, TSecretRotationV2,
@ -84,7 +84,7 @@ export const SecretRotationV2Form = ({
} }
: { : {
type, type,
isAutoRotationEnabled: true, isAutoRotationEnabled: IS_ROTATION_DUAL_CREDENTIALS[type],
rotationInterval: DEFAULT_ROTATION_INTERVAL, rotationInterval: DEFAULT_ROTATION_INTERVAL,
rotateAtUtc: { rotateAtUtc: {
hours: 0, hours: 0,
@ -92,7 +92,7 @@ export const SecretRotationV2Form = ({
}, },
environment: currentWorkspace?.environments.find((env) => env.slug === envSlug), environment: currentWorkspace?.environments.find((env) => env.slug === envSlug),
secretPath, secretPath,
...rotationOption!.template ...(rotationOption!.template as object) // can't infer type since we don't know which specific type it is
}, },
reValidateMode: "onChange" reValidateMode: "onChange"
}); });
@ -142,7 +142,11 @@ export const SecretRotationV2Form = ({
setSelectedTabIndex((prev) => prev - 1); setSelectedTabIndex((prev) => prev - 1);
}; };
const { handleSubmit, trigger } = formMethods; const {
handleSubmit,
trigger,
formState: { isSubmitting }
} = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields); const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
@ -219,7 +223,12 @@ export const SecretRotationV2Form = ({
</Tab.Group> </Tab.Group>
</FormProvider> </FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4"> <div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button onClick={handleNext} colorSchema="secondary"> <Button
onClick={handleNext}
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema="secondary"
>
{isFinalStep ? `${secretRotation ? "Update" : "Create"} Secret Rotation` : "Next"} {isFinalStep ? `${secretRotation ? "Update" : "Create"} Secret Rotation` : "Next"}
</Button> </Button>
<Button onClick={handlePrev} colorSchema="secondary"> <Button onClick={handlePrev} colorSchema="secondary">

View File

@ -0,0 +1,70 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import { useAuth0ConnectionListClients } from "@app/hooks/api/appConnections/auth0";
import { TAuth0Client } from "@app/hooks/api/appConnections/auth0/types";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const Auth0ClientSecretRotationParametersFields = () => {
const { control, watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.Auth0ClientSecret;
}
>();
const connectionId = watch("connection.id");
const { data: clients, isPending: isClientsPending } = useAuth0ConnectionListClients(
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="Application"
helperText={
<Tooltip
className="max-w-md"
content={
<>
Ensure that your connection has the{" "}
<span className="font-semibold">read_clients</span> permission and the application
exists in the connection&#39;s audience.
</>
}
>
<div>
<span>Don&#39;t see the application you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isClientsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={clients?.find((client) => client.id === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TAuth0Client>)?.id ?? null);
}}
options={clients}
placeholder="Select an application..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
);
};

View File

@ -3,11 +3,13 @@ import { useFormContext } from "react-hook-form";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas"; import { TSecretRotationV2Form } from "../schemas";
import { SqlRotationParametersFields } from "./shared"; import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = { const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlRotationParametersFields, [SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.MsSqlCredentials]: SqlRotationParametersFields [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields
}; };
export const SecretRotationV2ParametersFields = () => { export const SecretRotationV2ParametersFields = () => {

Some files were not shown because too many files have changed in this diff Show More