From aec4ee905ee4bb0c6ee462a25002a8bb6e26c81a Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 24 Jul 2025 09:40:54 -0300 Subject: [PATCH] Add client secrets authentication on Azure CS app connection --- backend/src/lib/api-docs/constants.ts | 4 +- .../azure-client-secrets-connection-enums.ts | 3 +- .../azure-client-secrets-connection-fns.ts | 250 ++++++++++++------ ...azure-client-secrets-connection-schemas.ts | 55 +++- .../azure-client-secrets-connection-types.ts | 5 + frontend/src/helpers/appConnections.ts | 3 +- .../types/azure-client-secrets-connection.ts | 27 +- .../AppConnectionForm/AppConnectionForm.tsx | 4 +- .../AzureClientSecretsConnectionForm.tsx | 181 ++++++++++--- 9 files changed, 392 insertions(+), 140 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b6c00985a..a06df447d 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2245,7 +2245,9 @@ export const AppConnections = { }, 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: { code: "The OAuth code to use to connect with Azure DevOps.", diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts index 338126c1e..eb0521c64 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts @@ -1,3 +1,4 @@ export enum AzureClientSecretsConnectionMethod { - OAuth = "oauth" + OAuth = "oauth", + ClientSecret = "client-secret" } diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index 463e40e7c..fa2563078 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-case-declarations */ import { AxiosError, AxiosResponse } from "axios"; 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 { ExchangeCodeAzureResponse, + TAzureClientSecretsConnectionAccessTokenCredentials, TAzureClientSecretsConnectionConfig, TAzureClientSecretsConnectionCredentials } from "./azure-client-secrets-connection-types"; @@ -26,7 +28,10 @@ export const getAzureClientSecretsConnectionListItem = () => { return { name: "Azure Client Secrets" 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 }; }; @@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async ( kmsService: Pick ) => { 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) { @@ -63,34 +62,81 @@ export const getAzureConnectionAccessToken = async ( const { refreshToken } = credentials; 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( + 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( - 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: currentTime + data.expires_in * 1000, + refreshToken: data.refresh_token + }; - const updatedCredentials = { - ...credentials, - accessToken: data.access_token, - expiresAt: currentTime + data.expires_in * 1000, - refreshToken: data.refresh_token - }; + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId: appConnection.orgId, + kmsService + }); - const encryptedCredentials = await encryptAppConnectionCredentials({ - credentials: updatedCredentials, - orgId: appConnection.orgId, - kmsService - }); + await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials }); - 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( + 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) => { @@ -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(); - 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 | null = null; - let tokenError: AxiosError | null = null; - - try { - tokenResp = await request.post( - 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) { 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 | null = null; + let tokenError: AxiosError | null = null; + + try { + tokenResp = await request.post( + 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 { tenantId: inputCredentials.tenantId, accessToken: tokenResp.data.access_token, refreshToken: tokenResp.data.refresh_token, expiresAt: Date.now() + tokenResp.data.expires_in * 1000 }; + + case AzureClientSecretsConnectionMethod.ClientSecret: + const { tenantId, clientId, clientSecret } = inputCredentials; + try { + const { data: clientData } = await request.post( + 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: throw new InternalServerError({ message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}` diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts index 2b4e65a13..4d2c486d7 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts @@ -26,6 +26,32 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object 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", [ z.object({ method: z @@ -34,6 +60,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe( 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 .object({ - credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe( - AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials - ) + credentials: z + .union([ + AzureClientSecretsConnectionOAuthInputCredentialsSchema, + AzureClientSecretsConnectionAccessTokenInputCredentialsSchema + ]) + .optional() + .describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials) }) .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets)); @@ -59,6 +97,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection( z.object({ method: z.literal(AzureClientSecretsConnectionMethod.OAuth), credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema + }), + z.object({ + method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret), + credentials: AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema }) ]) ); @@ -69,6 +111,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion( credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({ tenantId: true }) + }), + BaseAzureClientSecretsConnectionSchema.extend({ + method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret), + credentials: AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema.pick({ + clientId: true, + tenantId: true + }) }) ]); diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts index fb20fbadd..f6aa932d7 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts @@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; import { + AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema, AzureClientSecretsConnectionOAuthOutputCredentialsSchema, AzureClientSecretsConnectionSchema, CreateAzureClientSecretsConnectionSchema, @@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer< typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema >; +export type TAzureClientSecretsConnectionAccessTokenCredentials = z.infer< + typeof AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema +>; + export interface ExchangeCodeAzureResponse { token_type: string; scope: string; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 1345cb493..8c8475767 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -171,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case RenderConnectionMethod.ApiKey: case ChecklyConnectionMethod.ApiKey: return { name: "API Key", icon: faKey }; - + case AzureClientSecretsConnectionMethod.ClientSecret: + return { name: "Client Secret", icon: faKey }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/hooks/api/appConnections/types/azure-client-secrets-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-client-secrets-connection.ts index 04ad167d2..220e3cdb2 100644 --- a/frontend/src/hooks/api/appConnections/types/azure-client-secrets-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/azure-client-secrets-connection.ts @@ -2,15 +2,26 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; export enum AzureClientSecretsConnectionMethod { - OAuth = "oauth" + OAuth = "oauth", + ClientSecret = "client-secret" } export type TAzureClientSecretsConnection = TRootAppConnection & { app: AppConnection.AzureClientSecrets; -} & { - method: AzureClientSecretsConnectionMethod.OAuth; - credentials: { - code: string; - tenantId: string; - }; -}; +} & ( + | { + method: AzureClientSecretsConnectionMethod.OAuth; + credentials: { + code: string; + tenantId: string; + }; + } + | { + method: AzureClientSecretsConnectionMethod.ClientSecret; + credentials: { + clientSecret: string; + clientId: string; + tenantId: string; + }; + } + ); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index fa8c85b66..d4b87af81 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { case AppConnection.Camunda: return ; case AppConnection.AzureClientSecrets: - return ; + return ; case AppConnection.AzureDevOps: return ; case AppConnection.Windmill: @@ -222,7 +222,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { case AppConnection.Camunda: return ; case AppConnection.AzureClientSecrets: - return ; + return ; case AppConnection.AzureDevOps: return ; case AppConnection.Windmill: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx index 9076c95ce..927189e6d 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-case-declarations */ import crypto from "crypto"; import { useState } from "react"; @@ -20,19 +21,83 @@ import { GenericAppConnectionsFields } from "./GenericAppConnectionFields"; +type ClientSecretForm = z.infer; + type Props = { appConnection?: TAzureClientSecretsConnection; + onSubmit: (formData: ClientSecretForm) => Promise; }; -const formSchema = genericAppConnectionFieldsSchema.extend({ +const baseSchema = genericAppConnectionFieldsSchema.extend({ app: z.literal(AppConnection.AzureClientSecrets), - method: z.nativeEnum(AzureClientSecretsConnectionMethod), - tenantId: z.string().trim().min(1, "Tenant ID is required") + method: z.nativeEnum(AzureClientSecretsConnectionMethod) }); +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; -export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => { +const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partial => { + 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 [isRedirecting, setIsRedirecting] = useState(false); @@ -43,70 +108,51 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => { const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: appConnection - ? { - ...appConnection, - tenantId: appConnection.credentials.tenantId - } - : { - app: AppConnection.AzureClientSecrets, - method: AzureClientSecretsConnectionMethod.OAuth - } + defaultValues: getDefaultValues(appConnection) }); const { handleSubmit, control, watch, + setValue, formState: { isSubmitting, isDirty } } = form; const selectedMethod = watch("method"); - const onSubmit = (formData: FormData) => { - setIsRedirecting(true); + const onSubmitHandler = (formData: FormData) => { const state = crypto.randomBytes(16).toString("hex"); - localStorage.setItem("latestCSRFToken", state); - localStorage.setItem( - "azureClientSecretsConnectionFormData", - JSON.stringify({ ...formData, connectionId: appConnection?.id }) - ); - switch (formData.method) { case AzureClientSecretsConnectionMethod.OAuth: + setIsRedirecting(true); + localStorage.setItem("latestCSRFToken", state); + localStorage.setItem( + "azureClientSecretsConnectionFormData", + JSON.stringify({ ...formData, connectionId: appConnection?.id }) + ); 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` ); break; + + case AzureClientSecretsConnectionMethod.ClientSecret: + onSubmit(formData); + break; default: throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`); } }; - const isMissingConfig = !oauthClientId; - + const isMissingConfig = + selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId; const methodDetails = getAppConnectionMethodDetails(selectedMethod); return ( -
+ {!isUpdate && } - ( - - - - )} - /> - { )} /> + + ( + + { + field.onChange(e.target.value); + setValue("credentials.tenantId", e.target.value); + }} + /> + + )} + /> + + {/* Access Token-specific fields */} + {selectedMethod === AzureClientSecretsConnectionMethod.ClientSecret && ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + + )} +