improvements: address feedback

This commit is contained in:
Scott Wilson
2025-04-15 14:22:41 -07:00
parent 581e4b35f9
commit 98447e9402
23 changed files with 279 additions and 40 deletions

View File

@@ -10,6 +10,7 @@ import {
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";
@@ -26,11 +27,13 @@ export const auth0ClientSecretRotationFactory: TRotationFactory<
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: `${connection.credentials.audience}clients/${clientId}`,
url: `${audience}clients/${clientId}`,
headers: { authorization: `Bearer ${accessToken}` },
data: {
client_secret: clientSecret
@@ -53,11 +56,13 @@ export const auth0ClientSecretRotationFactory: TRotationFactory<
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: `${connection.credentials.audience}clients/${clientId}/rotate-secret`,
url: `${audience}clients/${clientId}/rotate-secret`,
headers: { authorization: `Bearer ${accessToken}` }
});

View File

@@ -28,7 +28,7 @@ const Auth0ClientSecretRotationParametersSchema = z.object({
});
const Auth0ClientSecretRotationSecretsMappingSchema = z.object({
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientID),
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientId),
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientSecret)
});

View File

@@ -1979,7 +1979,7 @@ export const SecretRotations = {
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.",
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

@@ -1,9 +1,14 @@
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";
@@ -15,4 +20,32 @@ export const registerAuth0ConnectionRouter = async (server: FastifyZodProvider)
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

@@ -14,6 +14,7 @@ import {
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
validateAppConnectionCredentials
} 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 { TAppConnectionDALFactory } from "./app-connection-dal";
@@ -440,6 +441,7 @@ export const appConnectionServiceFactory = ({
aws: awsConnectionService(connectAppConnectionById),
humanitec: humanitecConnectionService(connectAppConnectionById),
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById)
vercel: vercelConnectionService(connectAppConnectionById),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
};
};

View File

@@ -1,5 +1,3 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
@@ -45,7 +43,11 @@ const authorizeAuth0Connection = async ({
throw new Error(`Unhandled token type: ${data.token_type}`);
}
return { accessToken: data.access_token, expiresAt: data.expires_in * 1000 + Date.now() };
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 (
@@ -53,7 +55,12 @@ export const getAuth0ConnectionAccessToken = async (
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
// just getting a new auth token every time because if permissions change the previous token doesn't have the changes
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);
@@ -84,7 +91,7 @@ export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0
};
} catch (e: unknown) {
throw new BadRequestError({
message: (e as AxiosError).message ?? `Unable to validate connection: verify credentials`
message: (e as Error).message ?? `Unable to validate connection: verify credentials`
});
}
};

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

@@ -27,3 +27,13 @@ export type TAuth0AccessTokenResponse = {
scope: string;
token_type: string;
};
export type TAuth0ListClient = {
name: string;
client_id: string;
};
export type TAuth0ListClientsResponse = {
total: number;
clients: TAuth0ListClient[];
};

View File

@@ -13,11 +13,7 @@ description: "Learn how to automatically rotate Auth0 Client Secrets."
## Prerequisites
1. Create an [Auth0 Connection](/integrations/app-connections/auth0) with the required **Secret Rotation** audience and permissions
2. Copy the **Client ID** of the application in Auth0 you want to rotate the Client Secret for:
![Auth0 Client ID](/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png)
- Create an [Auth0 Connection](/integrations/app-connections/auth0) with the required **Secret Rotation** audience and permissions
## Create an Auth0 Client Secret Rotation in Infisical
@@ -36,12 +32,14 @@ description: "Learn how to automatically rotate Auth0 Client Secrets."
- **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. Input the Client ID of the Auth0 application acquired above that will have its Client Secret rotated. Then click **Next**.
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)
- **Client ID** - the Client ID of the application whose Client Secret will be rotated.
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)
@@ -64,6 +62,10 @@ description: "Learn how to automatically rotate Auth0 Client Secrets."
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
@@ -71,9 +73,9 @@ description: "Learn how to automatically rotate Auth0 Client Secrets."
--url https://us.infisical.com/api/v2/secret-rotations/auth0-client-secret \
--header 'Content-Type: application/json' \
--data '{
"name": "my-pg-rotation",
"name": "my-auth0-rotation",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "my database credentials rotation",
"description": "my client secret rotation",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/",
@@ -93,14 +95,18 @@ description: "Learn how to automatically rotate Auth0 Client Secrets."
}'
```
<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-pg-rotation",
"description": "my database credentials rotation",
"name": "my-auth0-rotation",
"description": "my client secret rotation",
"secretsMapping": {
"clientId": "AUTH0_CLIENT_ID",
"clientSecret": "AUTH0_CLIENT_SECRET"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 709 KiB

View File

@@ -1,6 +1,6 @@
---
title: "Auth0 Connection"
description: "Learn how to configure a Auth0 Connection for Infisical."
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.
@@ -22,7 +22,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st
<Tabs>
<Tab title="Secret Rotation">
Select the **Auth0 Management API** option from the dropdown and grant the `update:client_keys` permission.
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>
@@ -72,7 +72,7 @@ Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-st
"clientId": "...",
"clientSecret": "...",
"audience": "https://xxx-xxxxxxxxx.us.auth0.com/api/v2/"
},
}
}'
```

View File

@@ -1,8 +1,14 @@
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 { 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 { TSecretRotationV2Form } from "./schemas";
@@ -14,7 +20,9 @@ type Props = {
};
export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => {
const { control } = useFormContext<TSecretRotationV2Form>();
const { control, watch } = useFormContext<TSecretRotationV2Form>();
const [type, isAutoRotationEnabled] = watch(["type", "isAutoRotationEnabled"]);
return (
<>
@@ -117,6 +125,14 @@ 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-sm 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.
</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 { Button } from "@app/components/v2";
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 {
SecretRotation,
TSecretRotationV2,
@@ -84,7 +84,7 @@ export const SecretRotationV2Form = ({
}
: {
type,
isAutoRotationEnabled: true,
isAutoRotationEnabled: IS_ROTATION_DUAL_CREDENTIALS[type],
rotationInterval: DEFAULT_ROTATION_INTERVAL,
rotateAtUtc: {
hours: 0,

View File

@@ -1,30 +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 { FormControl, Input } from "@app/components/v2";
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 } = useFormContext<
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
tooltipText="The Client ID of the Auth0 Application you want to rotate the client secret for."
isError={Boolean(error)}
errorText={error?.message}
label="Client ID"
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>
}
>
<Input value={value} onChange={onChange} />
<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>
)}
control={control}
name="parameters.clientId"
/>
);
};

View File

@@ -8,7 +8,7 @@ import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secret
export const SqlCredentialsRotationParametersFields = () => {
const { control, watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.PostgresCredentials; // all sql rotations share these fields
type: SecretRotation.PostgresCredentials | SecretRotation.MsSqlCredentials;
}
>();

View File

@@ -9,7 +9,7 @@ import { SecretRotationReviewSection } from "./SecretRotationReviewSection";
export const SqlCredentialsRotationReviewFields = () => {
const { watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.PostgresCredentials; // all sql rotations share these fields
type: SecretRotation.PostgresCredentials | SecretRotation.MsSqlCredentials;
}
>();

View File

@@ -9,7 +9,7 @@ import { SecretsMappingTable } from "./SecretsMappingTable";
export const SqlCredentialsRotationSecretsMappingFields = () => {
const { control, watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.PostgresCredentials; // all sql rotations share these fields
type: SecretRotation.PostgresCredentials | SecretRotation.MsSqlCredentials;
}
>();

View File

@@ -28,6 +28,13 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
};
// if a rotation can potentially have downtime due to rotating a single credential set this to false
export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
[SecretRotation.PostgresCredentials]: true,
[SecretRotation.MsSqlCredentials]: true,
[SecretRotation.Auth0ClientSecret]: false
};
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
const now = new Date();

View File

@@ -0,0 +1 @@
export * from "./queries";

View File

@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TAuth0Client } from "./types";
const auth0ConnectionKeys = {
all: [...appConnectionKeys.all, "auth0"] as const,
listClients: (connectionId: string) =>
[...auth0ConnectionKeys.all, "clients", connectionId] as const
};
export const useAuth0ConnectionListClients = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TAuth0Client[],
unknown,
TAuth0Client[],
ReturnType<typeof auth0ConnectionKeys.listClients>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: auth0ConnectionKeys.listClients(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ clients: TAuth0Client[] }>(
`/api/v1/app-connections/auth0/${connectionId}/clients`
);
return data.clients;
},
...options
});
};

View File

@@ -0,0 +1,4 @@
export type TAuth0Client = {
name: string;
id: string;
};

View File

@@ -70,7 +70,7 @@ export const Auth0ConnectionForm = ({ appConnection, onSubmit }: Props) => {
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Databricks].name
APP_CONNECTION_MAP[AppConnection.Auth0].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}