Merge pull request #4229 from Infisical/feat/azureClientSecretsNewAuth

Add client secrets authentication on Azure CS app connection
This commit is contained in:
carlosmonastyrski
2025-07-25 15:00:49 -03:00
committed by GitHub
11 changed files with 448 additions and 152 deletions
+3 -1
View File
@@ -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.",
@@ -1,3 +1,4 @@
export enum AzureClientSecretsConnectionMethod { export enum AzureClientSecretsConnectionMethod {
OAuth = "oauth" OAuth = "oauth",
ClientSecret = "client-secret"
} }
@@ -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,
TAzureClientSecretsConnectionClientSecretCredentials,
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,7 +62,13 @@ 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>( const { data } = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"), IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
new URLSearchParams({ new URLSearchParams({
@@ -91,6 +96,47 @@ export const getAzureConnectionAccessToken = async (
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
return data.access_token; return data.access_token;
case AzureClientSecretsConnectionMethod.ClientSecret:
const accessTokenCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId,
kmsService,
encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureClientSecretsConnectionClientSecretCredentials;
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = accessTokenCredentials;
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
return accessToken;
}
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,6 +144,8 @@ 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();
switch (method) {
case AzureClientSecretsConnectionMethod.OAuth:
if (!SITE_URL) { if (!SITE_URL) {
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
} }
@@ -153,14 +201,46 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
}); });
} }
switch (method) {
case AzureClientSecretsConnectionMethod.OAuth:
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}`
@@ -26,6 +26,36 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
expiresAt: z.number() expiresAt: z.number()
}); });
export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z.object({
clientId: z
.string()
.uuid()
.trim()
.min(1, "Client ID required")
.max(50, "Client ID must be at most 50 characters long")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId),
clientSecret: z
.string()
.trim()
.min(1, "Client Secret required")
.max(50, "Client Secret must be at most 50 characters long")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret),
tenantId: z
.string()
.uuid()
.trim()
.min(1, "Tenant ID required")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
});
export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = 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 +64,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: AzureClientSecretsConnectionClientSecretInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
)
}) })
]); ]);
@@ -43,9 +81,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,
AzureClientSecretsConnectionClientSecretInputCredentialsSchema
])
.optional()
.describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
@@ -59,6 +101,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: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
}) })
]) ])
); );
@@ -69,6 +115,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({ credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true tenantId: true
}) })
}),
BaseAzureClientSecretsConnectionSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true,
tenantId: true
})
}) })
]); ]);
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { import {
AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
AzureClientSecretsConnectionOAuthOutputCredentialsSchema, AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
AzureClientSecretsConnectionSchema, AzureClientSecretsConnectionSchema,
CreateAzureClientSecretsConnectionSchema, CreateAzureClientSecretsConnectionSchema,
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
>; >;
export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
>;
export interface ExchangeCodeAzureResponse { export interface ExchangeCodeAzureResponse {
token_type: string; token_type: string;
scope: string; scope: string;
Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

@@ -72,6 +72,38 @@ Infisical currently only supports one method for connecting to Azure, which is O
</Accordion> </Accordion>
<Accordion title="Client Secret Authentication">
Ensure your Azure application has the required permissions that Infisical needs for the Azure Client Secrets connection to work.
**Prerequisites:**
- An active Azure setup.
<Steps>
<Step title="Assign API permissions to the application">
For the Azure Client Secrets connection to work, assign the following permissions to your Azure application:
#### Required API Permissions
**Microsoft Graph**
- `Application.ReadWrite.All`
- `Application.ReadWrite.OwnedBy`
- `Application.ReadWrite.All` (Delegated)
- `Directory.ReadWrite.All` (Delegated)
- `User.Read` (Delegated)
**Azure App Configuration**
- `KeyValue.Delete` (Delegated)
- `KeyValue.Read` (Delegated)
- `KeyValue.Write` (Delegated)
**Access Key Vault**
- `user_impersonation` (Delegated)
![Azure client secrets](/images/integrations/azure-client-secrets/app-api-permissions.png)
</Step>
</Steps>
</Accordion>
## Setup Azure Connection in Infisical ## Setup Azure Connection in Infisical
<Steps> <Steps>
@@ -82,20 +114,30 @@ Infisical currently only supports one method for connecting to Azure, which is O
<Step title="Add Connection"> <Step title="Add Connection">
Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/client-secrets/select-connection.png) Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/client-secrets/select-connection.png)
</Step> </Step>
<Step title="Create Connection">
<Tabs>
<Tab title="OAuth">
<Step title="Authorize Connection"> <Step title="Authorize Connection">
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step. Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
Now select the **OAuth** method and click **Connect to Azure**. Now select the **OAuth** method and click **Connect to Azure**.
![Connect via Azure OAUth](/images/app-connections/azure/client-secrets/create-oauth-method.png) ![Connect via Azure OAUth](/images/app-connections/azure/client-secrets/create-oauth-method.png)
</Step> </Step>
<Step title="Grant Access"> <Step title="Grant Access">
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
you will be redirected back to Infisical's App Connections page. ![Azure Client Secrets you will be redirected back to Infisical's App Connections page. ![Azure Client Secrets
Authorization](/images/app-connections/azure/grant-access.png) Authorization](/images/app-connections/azure/grant-access.png)
</Step>
</Tab>
<Tab title="Client Secret">
<Step title="Create Connection">
Fill in the **Tenant ID**, **Client ID** and **Client Secret** fields with the Directory (Tenant) ID, Application (Client) ID and Client Secret you obtained in the previous step.
![Connect via Azure OAUth](/images/app-connections/azure/client-secrets/create-client-secrets-method.png)
</Step>
</Tab>
</Tabs>
</Step> </Step>
<Step title="Connection Created"> <Step title="Connection Created">
Your **Azure Client Secrets Connection** is now available for use. ![Azure Client Secrets](/images/app-connections/azure/client-secrets/oauth-connection.png) Your **Azure Client Secrets Connection** is now available for use. ![Azure Client Secrets](/images/app-connections/azure/client-secrets/oauth-connection.png)
+2 -1
View File
@@ -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; method: AzureClientSecretsConnectionMethod.OAuth;
credentials: { credentials: {
code: string; code: string;
tenantId: string; tenantId: string;
}; };
}
| {
method: AzureClientSecretsConnectionMethod.ClientSecret;
credentials: {
clientSecret: string;
clientId: string;
tenantId: string;
}; };
}
);
@@ -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:
@@ -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");
switch (formData.method) {
case AzureClientSecretsConnectionMethod.OAuth:
setIsRedirecting(true);
localStorage.setItem("latestCSRFToken", state); localStorage.setItem("latestCSRFToken", state);
localStorage.setItem( localStorage.setItem(
"azureClientSecretsConnectionFormData", "azureClientSecretsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id }) JSON.stringify({ ...formData, connectionId: appConnection?.id })
); );
switch (formData.method) {
case AzureClientSecretsConnectionMethod.OAuth:
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,61 @@ 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="00000000-0000-0000-0000-000000000000"
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
isError={Boolean(error?.message)}
label="Client ID"
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
</FormControl>
)}
/>
<Controller
name="credentials.clientSecret"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
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"