Address PR comments for Azure Client Secret Rotation

This commit is contained in:
carlosmonastyrski
2025-04-29 20:19:30 -03:00
parent 511becabd8
commit 2a28d74bde
16 changed files with 54 additions and 44 deletions
@@ -1,3 +1,5 @@
import { AxiosError } from "axios";
import { import {
AzureAddPasswordResponse, AzureAddPasswordResponse,
TAzureClientSecretRotationGeneratedCredentials, TAzureClientSecretRotationGeneratedCredentials,
@@ -11,7 +13,7 @@ import {
TRotationFactoryRotateCredentials TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { BadRequestError } from "@app/lib/errors";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets";
const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0"; const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
@@ -23,8 +25,7 @@ export const azureClientSecretRotationFactory: TRotationFactory<
const { const {
connection, connection,
parameters: { appId }, parameters: { appId },
secretsMapping, secretsMapping
rotationInterval
} = secretRotation; } = secretRotation;
/** /**
@@ -34,11 +35,6 @@ export const azureClientSecretRotationFactory: TRotationFactory<
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService); const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
const endpoint = `${GRAPH_API_BASE}/applications/${appId}/addPassword`; const endpoint = `${GRAPH_API_BASE}/applications/${appId}/addPassword`;
await blockLocalAndPrivateIpAddresses(endpoint);
const endDateTime = new Date();
endDateTime.setDate(endDateTime.getDate() + rotationInterval);
const now = new Date(); const now = new Date();
const formattedDate = `${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart( const formattedDate = `${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(
2, 2,
@@ -50,8 +46,7 @@ export const azureClientSecretRotationFactory: TRotationFactory<
endpoint, endpoint,
{ {
passwordCredential: { passwordCredential: {
displayName: `Infisical Rotated Secret (${formattedDate})`, displayName: `Infisical Rotated Secret (${formattedDate})`
endDateTime: endDateTime.toISOString()
} }
}, },
{ {
@@ -70,9 +65,15 @@ export const azureClientSecretRotationFactory: TRotationFactory<
clientSecret: data.secretText, clientSecret: data.secretText,
clientId: data.keyId clientId: data.keyId
}; };
} catch (err: unknown) { } catch (error: unknown) {
const message = err instanceof Error ? err.message : String(err); if (error instanceof AxiosError) {
throw new Error(`Failed to add client secret to Azure app ${appId}: ${message}`); throw new BadRequestError({
message: `Failed to add client secret to Azure app ${appId}: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
} }
}; };
@@ -83,8 +84,6 @@ export const azureClientSecretRotationFactory: TRotationFactory<
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService); const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
const endpoint = `${GRAPH_API_BASE}/applications/${appId}/removePassword`; const endpoint = `${GRAPH_API_BASE}/applications/${appId}/removePassword`;
await blockLocalAndPrivateIpAddresses(endpoint);
try { try {
await request.post( await request.post(
endpoint, endpoint,
@@ -96,9 +95,17 @@ export const azureClientSecretRotationFactory: TRotationFactory<
} }
} }
); );
} catch (err: unknown) { } catch (error: unknown) {
const message = err instanceof Error ? err.message : String(err); if (error instanceof AxiosError) {
throw new Error(`Failed to remove client secret with keyId ${clientId} from app ${appId}: ${message}`); throw new BadRequestError({
message: `Failed to remove client secret with keyId ${clientId} from app ${appId}: ${
error.message || "Unknown error"
}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
} }
}; };
@@ -21,11 +21,7 @@ export const AzureClientSecretRotationGeneratedCredentialsSchema = z
const AzureClientSecretRotationParametersSchema = z.object({ const AzureClientSecretRotationParametersSchema = z.object({
appId: z.string().trim().min(1, "App ID Required").describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appId), appId: z.string().trim().min(1, "App ID Required").describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appId),
appName: z appName: z.string().trim().describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appName).optional()
.string()
.trim()
.min(1, "App Name Required")
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appName)
}); });
const AzureClientSecretRotationSecretsMappingSchema = z.object({ const AzureClientSecretRotationSecretsMappingSchema = z.object({
+4
View File
@@ -1875,6 +1875,10 @@ export const AppConnections = {
TEAMCITY: { TEAMCITY: {
instanceUrl: "The TeamCity instance URL to connect with.", instanceUrl: "The TeamCity instance URL to connect with.",
accessToken: "The access token to use to connect with TeamCity." accessToken: "The access token to use to connect with TeamCity."
},
AZURE_CLIENT_SECRETS: {
code: "The OAuth code to use to connect with Azure Client Secrets.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
} }
} }
}; };
@@ -11,12 +11,16 @@ import {
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums"; import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
export const AzureClientSecretsConnectionOAuthInputCredentialsSchema = z.object({ export const AzureClientSecretsConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required"), code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.code),
tenantId: z.string().trim().optional() tenantId: z
.string()
.trim()
.min(1, "Tenant ID required")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
}); });
export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object({ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object({
tenantId: z.string().optional(), tenantId: z.string(),
accessToken: z.string(), accessToken: z.string(),
refreshToken: z.string(), refreshToken: z.string(),
expiresAt: z.number() expiresAt: z.number()
@@ -1,6 +1,5 @@
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
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 { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns";
@@ -26,7 +25,6 @@ const listAzureRegisteredApps = async (
const accessToken = await getAzureConnectionAccessToken(appConnection.id, appConnectionDAL, kmsService); const accessToken = await getAzureConnectionAccessToken(appConnection.id, appConnectionDAL, kmsService);
const graphEndpoint = `https://graph.microsoft.com/v1.0/applications`; const graphEndpoint = `https://graph.microsoft.com/v1.0/applications`;
await blockLocalAndPrivateIpAddresses(graphEndpoint);
const apps: TAzureRegisteredApp[] = []; const apps: TAzureRegisteredApp[] = [];
let nextLink = graphEndpoint; let nextLink = graphEndpoint;
@@ -4,6 +4,7 @@ openapi: "POST /api/v1/app-connections/azure-client-secrets"
--- ---
<Note> <Note>
Check out the configuration docs for [Azure Client Secret Connections](/integrations/app-connections/azure-client-secrets) to learn how to obtain the Azure Client Secret Connections must be created through the Infisical UI.
required credentials. Check out the configuration docs for [Azure Client Secret Connections](/integrations/app-connections/azure-client-secrets) for a step-by-step
guide.
</Note> </Note>
@@ -4,6 +4,7 @@ openapi: "PATCH /api/v1/app-connections/azure-client-secrets/{connectionId}"
--- ---
<Note> <Note>
Check out the configuration docs for [Azure Client Secret Connections](/integrations/app-connections/azure-client-secrets) to learn how to obtain the Azure Client Secret Connections must be updated through the Infisical UI.
required credentials. Check out the configuration docs for [Azure Client Secret Connections](/integrations/app-connections/azure-client-secrets) for a step-by-step
guide.
</Note> </Note>
@@ -5,7 +5,7 @@ description: "Learn how to automatically rotate Azure Client Secrets."
## Prerequisites ## Prerequisites
- Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets) with the required **Secret Rotation** audience and permissions - Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets).
## Create an Azure Client Secret Rotation in Infisical ## Create an Azure Client Secret Rotation in Infisical
Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

After

Width:  |  Height:  |  Size: 576 KiB

@@ -42,9 +42,9 @@ Infisical currently only supports one method for connecting to Azure, which is O
</Step> </Step>
<Step title="Add your application credentials to Infisical"> <Step title="Add your application credentials to Infisical">
Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. Obtain the **Application (Client) ID** and **Directory (Tenant) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application.
![Azure client secrets](../../images/integrations/azure-app-configuration/config-credentials-1.png) ![Azure client secrets](../../images/app-connections/azure/client-secrets/config-credentials-1.png)
![Azure client secrets](../../images/integrations/azure-app-configuration/config-credentials-2.png) ![Azure client secrets](../../images/integrations/azure-app-configuration/config-credentials-2.png)
![Azure client secrets](../../images/integrations/azure-app-configuration/config-credentials-3.png) ![Azure client secrets](../../images/integrations/azure-app-configuration/config-credentials-3.png)
+1 -1
View File
@@ -433,8 +433,8 @@
"integrations/app-connections/auth0", "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-client-secrets", "integrations/app-connections/azure-client-secrets",
"integrations/app-connections/azure-key-vault",
"integrations/app-connections/camunda", "integrations/app-connections/camunda",
"integrations/app-connections/databricks", "integrations/app-connections/databricks",
"integrations/app-connections/gcp", "integrations/app-connections/gcp",
@@ -38,8 +38,8 @@ export const AzureClientSecretRotationParametersFields = () => {
content={ content={
<> <>
Ensure that your connection has the{" "} Ensure that your connection has the{" "}
<span className="font-semibold">read_clients</span> permission and the application <span className="font-semibold">Application.ReadWrite.All</span> permission and
exists in the connection&#39;s audience. the application exists in Azure.
</> </>
} }
> >
+1 -1
View File
@@ -23,7 +23,7 @@ export const SECRET_ROTATION_MAP: Record<
[SecretRotation.AzureClientSecret]: { [SecretRotation.AzureClientSecret]: {
name: "Azure Client Secret", name: "Azure Client Secret",
image: "Microsoft Azure.png", image: "Microsoft Azure.png",
size: 35 size: 65
}, },
[SecretRotation.LdapPassword]: { [SecretRotation.LdapPassword]: {
name: "LDAP Password", name: "LDAP Password",
@@ -11,6 +11,6 @@ export type TAzureClientSecretsConnection = TRootAppConnection & {
method: AzureClientSecretsConnectionMethod.OAuth; method: AzureClientSecretsConnectionMethod.OAuth;
credentials: { credentials: {
code: string; code: string;
tenantId?: string; tenantId: string;
}; };
}; };
@@ -27,7 +27,7 @@ type Props = {
const formSchema = genericAppConnectionFieldsSchema.extend({ const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureClientSecrets), app: z.literal(AppConnection.AzureClientSecrets),
method: z.nativeEnum(AzureClientSecretsConnectionMethod), method: z.nativeEnum(AzureClientSecretsConnectionMethod),
tenantId: z.string().trim().optional() tenantId: z.string().trim()
}); });
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -97,10 +97,9 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
control={control} control={control}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
tooltipText="The active Directory (Entra ID) Tenant ID." tooltipText="The Active Directory (Entra ID) Tenant ID."
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
label="Tenant ID" label="Tenant ID"
isOptional
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" /> <Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />