mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 20:51:50 +00:00
Add Azure Client Secrets Rotation
This commit is contained in:
@@ -33,6 +33,7 @@ import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
|
||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
@@ -448,6 +449,7 @@ export const appConnectionServiceFactory = ({
|
||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
};
|
||||
};
|
||||
|
||||
+78
-5
@@ -2,15 +2,22 @@ import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import {
|
||||
decryptAppConnectionCredentials,
|
||||
encryptAppConnectionCredentials,
|
||||
getAppConnectionMethodName
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureClientSecretsConnectionConfig
|
||||
TAzureClientSecretsConnectionConfig,
|
||||
TAzureClientSecretsConnectionCredentials
|
||||
} from "./azure-client-secrets-connection-types";
|
||||
|
||||
export const getAzureClientSecretsConnectionListItem = () => {
|
||||
@@ -24,6 +31,72 @@ export const getAzureClientSecretsConnectionListItem = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getAzureConnectionAccessToken = async (
|
||||
connectionId: string,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||
}
|
||||
|
||||
if (appConnection.app !== AppConnection.AzureClientSecrets) {
|
||||
throw new BadRequestError({
|
||||
message: `Connection with ID '${connectionId}' is not an Azure Client Secrets connection`
|
||||
});
|
||||
}
|
||||
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureClientSecretsConnectionCredentials;
|
||||
|
||||
const { expiresAt, refreshToken } = credentials;
|
||||
|
||||
// get new token if expired or less than 5 minutes until expiry
|
||||
if (Date.now() < expiresAt - 300000) {
|
||||
return credentials.accessToken;
|
||||
}
|
||||
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
refresh_token: refreshToken
|
||||
})
|
||||
);
|
||||
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: Date.now() + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||
|
||||
return data.access_token;
|
||||
};
|
||||
|
||||
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
||||
const { credentials: inputCredentials, method } = config;
|
||||
|
||||
@@ -44,10 +117,10 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: inputCredentials.code,
|
||||
scope: `openid offline_access https://azconfig.io/.default`,
|
||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure-client-secrets/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
|
||||
+30
-5
@@ -26,7 +26,11 @@ export type TAzureClientSecretsConnectionConfig = DiscriminativePick<
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type ExchangeCodeAzureResponse = {
|
||||
export type TAzureClientSecretsConnectionCredentials = z.infer<
|
||||
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export interface ExchangeCodeAzureResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
@@ -34,8 +38,29 @@ export type ExchangeCodeAzureResponse = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
id_token: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TAzureRegisteredApp {
|
||||
id: string;
|
||||
appId: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
createdDateTime: string;
|
||||
identifierUris?: string[];
|
||||
signInAudience?: string;
|
||||
}
|
||||
|
||||
export interface TAzureListRegisteredAppsResponse {
|
||||
"@odata.context": string;
|
||||
"@odata.nextLink"?: string;
|
||||
value: TAzureRegisteredApp[];
|
||||
}
|
||||
|
||||
export interface TAzureClientSecret {
|
||||
keyId: string;
|
||||
displayName?: string;
|
||||
startDateTime: string;
|
||||
endDateTime: string;
|
||||
secretText?: string;
|
||||
}
|
||||
|
||||
export type TAzureClientSecretsConnectionCredentials = z.infer<
|
||||
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
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 { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import {
|
||||
TAzureClientSecretsConnection,
|
||||
TAzureListRegisteredAppsResponse,
|
||||
TAzureRegisteredApp
|
||||
} from "./azure-client-secrets-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TAzureClientSecretsConnection>;
|
||||
|
||||
const listAzureRegisteredApps = async (
|
||||
appConnection: TAzureClientSecretsConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const accessToken = await getAzureConnectionAccessToken(appConnection.id, appConnectionDAL, kmsService);
|
||||
|
||||
const graphEndpoint = `https://graph.microsoft.com/v1.0/applications`;
|
||||
await blockLocalAndPrivateIpAddresses(graphEndpoint);
|
||||
|
||||
const apps: TAzureRegisteredApp[] = [];
|
||||
let nextLink = graphEndpoint;
|
||||
|
||||
while (nextLink) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: appsPage } = await request.get<TAzureListRegisteredAppsResponse>(nextLink, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
apps.push(...appsPage.value);
|
||||
nextLink = appsPage["@odata.nextLink"] || "";
|
||||
}
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
export const azureClientSecretsConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.AzureClientSecrets, connectionId, actor);
|
||||
|
||||
const apps = await listAzureRegisteredApps(appConnection, appConnectionDAL, kmsService);
|
||||
|
||||
return apps.map((app) => ({
|
||||
id: app.id,
|
||||
name: app.displayName,
|
||||
appId: app.appId
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
listApps
|
||||
};
|
||||
};
|
||||
+5
-1
@@ -38,7 +38,11 @@ export const getAzureConnectionAccessToken = async (
|
||||
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||
}
|
||||
|
||||
if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) {
|
||||
if (
|
||||
appConnection.app !== AppConnection.AzureKeyVault &&
|
||||
appConnection.app !== AppConnection.AzureAppConfiguration &&
|
||||
appConnection.app !== AppConnection.AzureClientSecrets
|
||||
) {
|
||||
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user