diff --git a/.changeset/tidy-donuts-attend.md b/.changeset/tidy-donuts-attend.md new file mode 100644 index 00000000000..82cccf3a5e8 --- /dev/null +++ b/.changeset/tidy-donuts-attend.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Add the remaining optional enterprise connection parameters supported by the Backend API. `CreateEnterpriseConnectionParams` and `UpdateEnterpriseConnectionParams` now accept `allowOrganizationAccountLinking`, `customAttributes`, `authenticatable`, and `disableJitProvisioning` (update also accepts `disableAdditionalIdentifications`), and SAML params accept `loginHint` for configuring the `login_hint` sent to the IdP. diff --git a/.changeset/violet-planes-repeat.md b/.changeset/violet-planes-repeat.md new file mode 100644 index 00000000000..4aee6753981 --- /dev/null +++ b/.changeset/violet-planes-repeat.md @@ -0,0 +1,9 @@ +--- +'@clerk/backend': patch +--- + +Align `CreateEnterpriseConnectionParams` and `UpdateEnterpriseConnectionParams` with the Backend API contract: + +- `name` and `domains` are now required on `CreateEnterpriseConnectionParams`. The Backend API already rejected requests missing either of them, so calls that omitted these fields failed at runtime; the types now surface this at compile time. +- Deprecated `syncUserAttributes` on `CreateEnterpriseConnectionParams`. The Backend API ignores this parameter on create; use `updateEnterpriseConnection()` to set it. +- Deprecated `provider` on `UpdateEnterpriseConnectionParams`. The Backend API ignores this parameter on update; the provider cannot be changed after creation. diff --git a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts index d9675e0080c..f46f0281e8f 100644 --- a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts +++ b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts @@ -2,7 +2,10 @@ import { http, HttpResponse } from 'msw'; import { describe, expect, expectTypeOf, it } from 'vitest'; import { server, validateHeaders } from '../../mock-server'; -import type { CreateEnterpriseConnectionParams } from '../endpoints/EnterpriseConnectionApi'; +import type { + CreateEnterpriseConnectionParams, + EnterpriseConnectionSamlLoginHintParams, +} from '../endpoints/EnterpriseConnectionApi'; import { createBackendApiClient } from '../factory'; describe('EnterpriseConnectionAPI', () => { @@ -129,6 +132,76 @@ describe('EnterpriseConnectionAPI', () => { expectTypeOf<'saml_bogus'>().not.toExtend(); expectTypeOf<'saml_okta'>().toExtend(); }); + + it('requires name and domains at the type level', () => { + expectTypeOf<{ + name: string; + domains: string[]; + provider: 'saml_custom'; + }>().toExtend(); + expectTypeOf<{ domains: string[]; provider: 'saml_custom' }>().not.toExtend(); + expectTypeOf<{ name: string; provider: 'saml_custom' }>().not.toExtend(); + }); + + it('requires a login hint source exactly when mode is custom_attribute at the type level', () => { + expectTypeOf<{ mode: 'custom_attribute'; source: string }>().toExtend(); + expectTypeOf<{ mode: 'email_address' }>().toExtend(); + expectTypeOf<{ mode: 'custom_attribute' }>().not.toExtend(); + expectTypeOf<{ mode: 'off'; source: string }>().not.toExtend(); + }); + + it('sends provisioning, custom attribute, and login hint params in snake_case', async () => { + server.use( + http.post( + 'https://api.clerk.test/v1/enterprise_connections', + validateHeaders(async ({ request }) => { + const body = (await request.json()) as Record; + + expect(body.allow_organization_account_linking).toBe(true); + expect(body.authenticatable).toBe(false); + expect(body.disable_jit_provisioning).toBe(true); + expect(body.custom_attributes).toEqual([ + { + name: 'Employee Number', + key: 'employee_number', + sso_path: 'user.employeeNumber', + multi_valued: false, + }, + ]); + expect((body.saml as Record).login_hint).toEqual({ + mode: 'custom_attribute', + source: 'employee_number', + }); + + return HttpResponse.json(mockEnterpriseConnectionResponse); + }), + ), + ); + + await apiClient.enterpriseConnections.createEnterpriseConnection({ + name: 'Clerk', + domains: ['clerk.dev'], + provider: 'saml_custom', + allowOrganizationAccountLinking: true, + authenticatable: false, + disableJitProvisioning: true, + customAttributes: [ + { + name: 'Employee Number', + key: 'employee_number', + ssoPath: 'user.employeeNumber', + multiValued: false, + }, + ], + saml: { + idpEntityId: 'xxx', + loginHint: { + mode: 'custom_attribute', + source: 'employee_number', + }, + }, + }); + }); }); describe('updateEnterpriseConnection', () => { @@ -157,6 +230,47 @@ describe('EnterpriseConnectionAPI', () => { }, }); }); + + it('sends provisioning and custom attribute params in snake_case', async () => { + server.use( + http.patch( + 'https://api.clerk.test/v1/enterprise_connections/entconn_123', + validateHeaders(async ({ request }) => { + const body = (await request.json()) as Record; + + expect(body.sync_user_attributes).toBe(true); + expect(body.disable_additional_identifications).toBe(true); + expect(body.allow_organization_account_linking).toBe(false); + expect(body.authenticatable).toBe(true); + expect(body.disable_jit_provisioning).toBe(false); + expect(body.custom_attributes).toEqual([ + { + name: 'Department', + key: 'department', + scim_path: 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department', + }, + ]); + + return HttpResponse.json(mockEnterpriseConnectionResponse); + }), + ), + ); + + await apiClient.enterpriseConnections.updateEnterpriseConnection('entconn_123', { + syncUserAttributes: true, + disableAdditionalIdentifications: true, + allowOrganizationAccountLinking: false, + authenticatable: true, + disableJitProvisioning: false, + customAttributes: [ + { + name: 'Department', + key: 'department', + scimPath: 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department', + }, + ], + }); + }); }); describe('getEnterpriseConnectionList', () => { diff --git a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts index 3a4a246fce2..644fc0c88d8 100644 --- a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts +++ b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts @@ -49,6 +49,35 @@ export interface EnterpriseConnectionSamlAttributeMappingParams { lastName?: string | null; } +/** @inline */ +export type EnterpriseConnectionSamlLoginHintParams = + | { + /** Sends the value stored at the user `publicMetadata` key named by `source` as the `login_hint`. */ + mode: 'custom_attribute'; + /** The user `publicMetadata` key to read the `login_hint` value from. */ + source: string; + } + | { + /** How the SAML connection emits the `login_hint` sent to the IdP: `'email_address'` sends the typed identifier and `'off'` omits the `login_hint`. */ + mode: 'email_address' | 'off'; + /** Only supported when `mode` is `'custom_attribute'`. */ + source?: never; + }; + +/** @inline */ +export interface EnterpriseConnectionCustomAttributeParams { + /** The display name of the custom attribute. */ + name: string; + /** The key to store the custom attribute under. */ + key: string; + /** The SSO (SAML or OIDC) attribute path to read the value from. */ + ssoPath?: string; + /** The SCIM attribute path to read the value from. */ + scimPath?: string; + /** Whether the custom attribute holds multiple values. */ + multiValued?: boolean; +} + /** @inline */ export interface EnterpriseConnectionSamlParams { /** Whether the SAML connection allows Identity Provider (IdP) initiated flows. */ @@ -69,22 +98,35 @@ export interface EnterpriseConnectionSamlParams { idpMetadataUrl?: string; /** The IdP Single-Sign On URL for the SAML connection. */ idpSsoUrl?: string; + /** Configuration for the `login_hint` the SAML connection sends to the IdP. */ + loginHint?: EnterpriseConnectionSamlLoginHintParams; } /** @generateWithEmptyComment */ export type CreateEnterpriseConnectionParams = { /** The name of the enterprise connection. */ - name?: string; - /** The [Verified Domains](https://clerk.com/docs/guides/organizations/add-members/verified-domains) of the enterprise connection. */ - domains?: string[]; + name: string; + /** The [Verified Domains](https://clerk.com/docs/guides/organizations/add-members/verified-domains) of the enterprise connection. Must contain at least one domain. */ + domains: string[]; /** The organization ID of the enterprise connection. */ organizationId?: string; /** Whether the enterprise connection should be active. */ active?: boolean; - /** Whether the enterprise connection should sync user attributes between the IdP and Clerk. */ + /** + * Whether the enterprise connection should sync user attributes between the IdP and Clerk. + * @deprecated The Backend API does not support this parameter on create and ignores it. Use `updateEnterpriseConnection()` to set it. + */ syncUserAttributes?: boolean; /** The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. */ provider: OrganizationEnterpriseConnectionProvider; + /** Whether existing users who are members of the organization can link their account to their enterprise identity. If `false`, only sign-up flows apply. */ + allowOrganizationAccountLinking?: boolean; + /** The custom attribute mappings for the enterprise connection. */ + customAttributes?: EnterpriseConnectionCustomAttributeParams[]; + /** Whether the enterprise connection can be used for sign-in and sign-up. Requires the authenticatable enterprise connections feature to be enabled for the instance. */ + authenticatable?: boolean; + /** Whether Just-in-Time (JIT) provisioning of users is disabled for the enterprise connection. */ + disableJitProvisioning?: boolean; /** Configuration for if the enterprise connection uses OAuth (OIDC). */ oidc?: EnterpriseConnectionOidcParams; /** Configuration for if the enterprise connection uses SAML. */ @@ -103,7 +145,20 @@ export type UpdateEnterpriseConnectionParams = { active?: boolean; /** Whether the enterprise connection should sync user attributes between the IdP and Clerk. */ syncUserAttributes?: boolean; - /** The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. */ + /** Whether additional identifications are disabled for the enterprise connection. */ + disableAdditionalIdentifications?: boolean; + /** Whether existing users who are members of the organization can link their account to their enterprise identity. If `false`, only sign-up flows apply. */ + allowOrganizationAccountLinking?: boolean; + /** The custom attribute mappings for the enterprise connection. */ + customAttributes?: EnterpriseConnectionCustomAttributeParams[]; + /** Whether the enterprise connection can be used for sign-in and sign-up. Requires the authenticatable enterprise connections feature to be enabled for the instance. */ + authenticatable?: boolean; + /** Whether Just-in-Time (JIT) provisioning of users is disabled for the enterprise connection. */ + disableJitProvisioning?: boolean; + /** + * The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. + * @deprecated The Backend API does not support this parameter on update and ignores it. The provider cannot be changed after creation. + */ provider?: string; /** Configuration for if the enterprise connection uses OAuth (OIDC). */ oidc?: EnterpriseConnectionOidcParams;