mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 11:26:17 +00:00
Add client secrets authentication on Azure CS app connection
This commit is contained in:
@@ -2245,7 +2245,9 @@ export const AppConnections = {
|
|||||||
},
|
},
|
||||||
AZURE_CLIENT_SECRETS: {
|
AZURE_CLIENT_SECRETS: {
|
||||||
code: "The OAuth code to use to connect with 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."
|
tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
|
||||||
|
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||||
|
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||||
},
|
},
|
||||||
AZURE_DEVOPS: {
|
AZURE_DEVOPS: {
|
||||||
code: "The OAuth code to use to connect with Azure DevOps.",
|
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
export enum AzureClientSecretsConnectionMethod {
|
export enum AzureClientSecretsConnectionMethod {
|
||||||
OAuth = "oauth"
|
OAuth = "oauth",
|
||||||
|
ClientSecret = "client-secret"
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-85
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
import { AxiosError, AxiosResponse } from "axios";
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -16,6 +17,7 @@ import { AppConnection } from "../app-connection-enums";
|
|||||||
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
||||||
import {
|
import {
|
||||||
ExchangeCodeAzureResponse,
|
ExchangeCodeAzureResponse,
|
||||||
|
TAzureClientSecretsConnectionAccessTokenCredentials,
|
||||||
TAzureClientSecretsConnectionConfig,
|
TAzureClientSecretsConnectionConfig,
|
||||||
TAzureClientSecretsConnectionCredentials
|
TAzureClientSecretsConnectionCredentials
|
||||||
} from "./azure-client-secrets-connection-types";
|
} from "./azure-client-secrets-connection-types";
|
||||||
@@ -26,7 +28,10 @@ export const getAzureClientSecretsConnectionListItem = () => {
|
|||||||
return {
|
return {
|
||||||
name: "Azure Client Secrets" as const,
|
name: "Azure Client Secrets" as const,
|
||||||
app: AppConnection.AzureClientSecrets as const,
|
app: AppConnection.AzureClientSecrets as const,
|
||||||
methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
|
methods: Object.values(AzureClientSecretsConnectionMethod) as [
|
||||||
|
AzureClientSecretsConnectionMethod.OAuth,
|
||||||
|
AzureClientSecretsConnectionMethod.ClientSecret
|
||||||
|
],
|
||||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
) => {
|
) => {
|
||||||
const appCfg = getConfig();
|
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);
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
if (!appConnection) {
|
if (!appConnection) {
|
||||||
@@ -63,34 +62,81 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
|
|
||||||
const { refreshToken } = credentials;
|
const { refreshToken } = credentials;
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Azure OAuth environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 { data } = await request.post<ExchangeCodeAzureResponse>(
|
const updatedCredentials = {
|
||||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
...credentials,
|
||||||
new URLSearchParams({
|
accessToken: data.access_token,
|
||||||
grant_type: "refresh_token",
|
expiresAt: currentTime + data.expires_in * 1000,
|
||||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
refreshToken: data.refresh_token
|
||||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
};
|
||||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
|
||||||
refresh_token: refreshToken
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedCredentials = {
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
...credentials,
|
credentials: updatedCredentials,
|
||||||
accessToken: data.access_token,
|
orgId: appConnection.orgId,
|
||||||
expiresAt: currentTime + data.expires_in * 1000,
|
kmsService
|
||||||
refreshToken: data.refresh_token
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||||
credentials: updatedCredentials,
|
|
||||||
orgId: appConnection.orgId,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
return data.access_token;
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
const accessTokenCredentials = (await decryptAppConnectionCredentials({
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials
|
||||||
|
})) as TAzureClientSecretsConnectionAccessTokenCredentials;
|
||||||
|
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = accessTokenCredentials;
|
||||||
|
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
return data.access_token;
|
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
scope: `https://graph.microsoft.com/.default`,
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedClientCredentials = {
|
||||||
|
...accessTokenCredentials,
|
||||||
|
accessToken: clientData.access_token,
|
||||||
|
expiresAt: currentTime + clientData.expires_in * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: updatedClientCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
|
||||||
|
|
||||||
|
return clientData.access_token;
|
||||||
|
default:
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Unhandled Azure connection method: ${appConnection.method as AzureClientSecretsConnectionMethod}`
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
||||||
@@ -98,69 +144,103 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
|||||||
|
|
||||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||||
|
|
||||||
if (!SITE_URL) {
|
|
||||||
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
|
||||||
let tokenError: AxiosError | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
|
||||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: "authorization_code",
|
|
||||||
code: inputCredentials.code,
|
|
||||||
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`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if (e instanceof AxiosError) {
|
|
||||||
tokenError = e;
|
|
||||||
} else {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Unable to validate connection: verify credentials`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenError) {
|
|
||||||
if (tokenError instanceof AxiosError) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Failed to get access token: ${
|
|
||||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
|
||||||
}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: "Failed to get access token"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenResp) {
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: `Failed to get access token: Token was empty with no error`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
if (!SITE_URL) {
|
||||||
|
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||||
|
let tokenError: AxiosError | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: inputCredentials.code,
|
||||||
|
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`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof AxiosError) {
|
||||||
|
tokenError = e;
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection: verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenError) {
|
||||||
|
if (tokenError instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to get access token: ${
|
||||||
|
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get access token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenResp) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Failed to get access token: Token was empty with no error`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tenantId: inputCredentials.tenantId,
|
tenantId: inputCredentials.tenantId,
|
||||||
accessToken: tokenResp.data.access_token,
|
accessToken: tokenResp.data.access_token,
|
||||||
refreshToken: tokenResp.data.refresh_token,
|
refreshToken: tokenResp.data.refresh_token,
|
||||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
const { tenantId, clientId, clientSecret } = inputCredentials;
|
||||||
|
try {
|
||||||
|
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
scope: `https://graph.microsoft.com/.default`,
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenantId,
|
||||||
|
accessToken: clientData.access_token,
|
||||||
|
expiresAt: Date.now() + clientData.expires_in * 1000,
|
||||||
|
clientId,
|
||||||
|
clientSecret
|
||||||
|
};
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to get access token: ${
|
||||||
|
(e?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get access token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new InternalServerError({
|
throw new InternalServerError({
|
||||||
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
||||||
|
|||||||
+52
-3
@@ -26,6 +26,32 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
|
|||||||
expiresAt: z.number()
|
expiresAt: z.number()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionAccessTokenInputCredentialsSchema = z.object({
|
||||||
|
clientId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client ID required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId),
|
||||||
|
clientSecret: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client Secret required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret),
|
||||||
|
tenantId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Tenant ID required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema = z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
accessToken: z.string(),
|
||||||
|
expiresAt: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
z.object({
|
z.object({
|
||||||
method: z
|
method: z
|
||||||
@@ -34,6 +60,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin
|
|||||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
||||||
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(AzureClientSecretsConnectionMethod.ClientSecret)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method),
|
||||||
|
credentials: AzureClientSecretsConnectionAccessTokenInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
||||||
|
)
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -43,9 +77,13 @@ export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecre
|
|||||||
|
|
||||||
export const UpdateAzureClientSecretsConnectionSchema = z
|
export const UpdateAzureClientSecretsConnectionSchema = z
|
||||||
.object({
|
.object({
|
||||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
|
credentials: z
|
||||||
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
|
.union([
|
||||||
)
|
AzureClientSecretsConnectionOAuthInputCredentialsSchema,
|
||||||
|
AzureClientSecretsConnectionAccessTokenInputCredentialsSchema
|
||||||
|
])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
||||||
|
|
||||||
@@ -59,6 +97,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
|
|||||||
z.object({
|
z.object({
|
||||||
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
||||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||||
|
credentials: AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -69,6 +111,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
|
|||||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
||||||
tenantId: true
|
tenantId: true
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
BaseAzureClientSecretsConnectionSchema.extend({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||||
|
credentials: AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema.pick({
|
||||||
|
clientId: true,
|
||||||
|
tenantId: true
|
||||||
|
})
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
+5
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
|||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import {
|
import {
|
||||||
|
AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema,
|
||||||
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
||||||
AzureClientSecretsConnectionSchema,
|
AzureClientSecretsConnectionSchema,
|
||||||
CreateAzureClientSecretsConnectionSchema,
|
CreateAzureClientSecretsConnectionSchema,
|
||||||
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
|
|||||||
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type TAzureClientSecretsConnectionAccessTokenCredentials = z.infer<
|
||||||
|
typeof AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export interface ExchangeCodeAzureResponse {
|
export interface ExchangeCodeAzureResponse {
|
||||||
token_type: string;
|
token_type: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
case RenderConnectionMethod.ApiKey:
|
case RenderConnectionMethod.ApiKey:
|
||||||
case ChecklyConnectionMethod.ApiKey:
|
case ChecklyConnectionMethod.ApiKey:
|
||||||
return { name: "API Key", icon: faKey };
|
return { name: "API Key", icon: faKey };
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
return { name: "Client Secret", icon: faKey };
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,26 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
|||||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||||
|
|
||||||
export enum AzureClientSecretsConnectionMethod {
|
export enum AzureClientSecretsConnectionMethod {
|
||||||
OAuth = "oauth"
|
OAuth = "oauth",
|
||||||
|
ClientSecret = "client-secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
||||||
app: AppConnection.AzureClientSecrets;
|
app: AppConnection.AzureClientSecrets;
|
||||||
} & {
|
} & (
|
||||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
| {
|
||||||
credentials: {
|
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||||
code: string;
|
credentials: {
|
||||||
tenantId: string;
|
code: string;
|
||||||
};
|
tenantId: string;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: AzureClientSecretsConnectionMethod.ClientSecret;
|
||||||
|
credentials: {
|
||||||
|
clientSecret: string;
|
||||||
|
clientId: string;
|
||||||
|
tenantId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
+2
-2
@@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
|||||||
case AppConnection.Camunda:
|
case AppConnection.Camunda:
|
||||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm />;
|
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureDevOps:
|
case AppConnection.AzureDevOps:
|
||||||
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
@@ -222,7 +222,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
case AppConnection.Camunda:
|
case AppConnection.Camunda:
|
||||||
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureDevOps:
|
case AppConnection.AzureDevOps:
|
||||||
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
|
|||||||
+142
-39
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -20,19 +21,83 @@ import {
|
|||||||
GenericAppConnectionsFields
|
GenericAppConnectionsFields
|
||||||
} from "./GenericAppConnectionFields";
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
|
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appConnection?: TAzureClientSecretsConnection;
|
appConnection?: TAzureClientSecretsConnection;
|
||||||
|
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
app: z.literal(AppConnection.AzureClientSecrets),
|
app: z.literal(AppConnection.AzureClientSecrets),
|
||||||
method: z.nativeEnum(AzureClientSecretsConnectionMethod),
|
method: z.nativeEnum(AzureClientSecretsConnectionMethod)
|
||||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oauthSchema = baseSchema.extend({
|
||||||
|
tenantId: z.string().trim().min(1, "Tenant ID is required"),
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.OAuth)
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientSecretSchema = baseSchema.extend({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||||
|
credentials: z.object({
|
||||||
|
clientSecret: z.string().trim().min(1, "Client Secret is required"),
|
||||||
|
clientId: z.string().trim().min(1, "Client ID is required"),
|
||||||
|
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partial<FormData> => {
|
||||||
|
if (!appConnection) {
|
||||||
|
return {
|
||||||
|
app: AppConnection.AzureClientSecrets,
|
||||||
|
method: AzureClientSecretsConnectionMethod.OAuth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
name: appConnection.name,
|
||||||
|
description: appConnection.description,
|
||||||
|
app: appConnection.app,
|
||||||
|
method: appConnection.method
|
||||||
|
};
|
||||||
|
const { credentials } = appConnection;
|
||||||
|
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
if ("tenantId" in credentials) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
method: AzureClientSecretsConnectionMethod.OAuth,
|
||||||
|
tenantId: credentials.tenantId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
if ("clientSecret" in credentials && "clientId" in credentials) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
method: AzureClientSecretsConnectionMethod.ClientSecret,
|
||||||
|
credentials: {
|
||||||
|
clientSecret: credentials.clientSecret,
|
||||||
|
clientId: credentials.clientId,
|
||||||
|
tenantId: credentials.tenantId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||||
const isUpdate = Boolean(appConnection);
|
const isUpdate = Boolean(appConnection);
|
||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
|
|
||||||
@@ -43,70 +108,51 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: appConnection
|
defaultValues: getDefaultValues(appConnection)
|
||||||
? {
|
|
||||||
...appConnection,
|
|
||||||
tenantId: appConnection.credentials.tenantId
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
app: AppConnection.AzureClientSecrets,
|
|
||||||
method: AzureClientSecretsConnectionMethod.OAuth
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const selectedMethod = watch("method");
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
const onSubmit = (formData: FormData) => {
|
const onSubmitHandler = (formData: FormData) => {
|
||||||
setIsRedirecting(true);
|
|
||||||
const state = crypto.randomBytes(16).toString("hex");
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
localStorage.setItem("latestCSRFToken", state);
|
|
||||||
localStorage.setItem(
|
|
||||||
"azureClientSecretsConnectionFormData",
|
|
||||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (formData.method) {
|
switch (formData.method) {
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
setIsRedirecting(true);
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
localStorage.setItem(
|
||||||
|
"azureClientSecretsConnectionFormData",
|
||||||
|
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||||
|
);
|
||||||
window.location.assign(
|
window.location.assign(
|
||||||
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
onSubmit(formData);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMissingConfig = !oauthClientId;
|
const isMissingConfig =
|
||||||
|
selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId;
|
||||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmitHandler)}>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="tenantId"
|
|
||||||
control={control}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
tooltipText="The Directory (tenant) ID."
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
label="Tenant ID"
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -146,6 +192,63 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="tenantId"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="The Directory (tenant) ID."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Tenant ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8"
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e.target.value);
|
||||||
|
setValue("credentials.tenantId", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Access Token-specific fields */}
|
||||||
|
{selectedMethod === AzureClientSecretsConnectionMethod.ClientSecret && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="credentials.clientId"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Your Azure Client ID."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Client ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="myclientid" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="credentials.clientSecret"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Your Azure Client Secret."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Client Secret"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" placeholder="Enter your Client Secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-8 flex items-center">
|
<div className="mt-8 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
Reference in New Issue
Block a user