Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-donuts-attend.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions .changeset/violet-planes-repeat.md
Original file line number Diff line number Diff line change
@@ -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.
116 changes: 115 additions & 1 deletion packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -129,6 +132,76 @@ describe('EnterpriseConnectionAPI', () => {
expectTypeOf<'saml_bogus'>().not.toExtend<CreateEnterpriseConnectionParams['provider']>();
expectTypeOf<'saml_okta'>().toExtend<CreateEnterpriseConnectionParams['provider']>();
});

it('requires name and domains at the type level', () => {
expectTypeOf<{
name: string;
domains: string[];
provider: 'saml_custom';
}>().toExtend<CreateEnterpriseConnectionParams>();
expectTypeOf<{ domains: string[]; provider: 'saml_custom' }>().not.toExtend<CreateEnterpriseConnectionParams>();
expectTypeOf<{ name: string; provider: 'saml_custom' }>().not.toExtend<CreateEnterpriseConnectionParams>();
});

it('requires a login hint source exactly when mode is custom_attribute at the type level', () => {
expectTypeOf<{ mode: 'custom_attribute'; source: string }>().toExtend<EnterpriseConnectionSamlLoginHintParams>();
expectTypeOf<{ mode: 'email_address' }>().toExtend<EnterpriseConnectionSamlLoginHintParams>();
expectTypeOf<{ mode: 'custom_attribute' }>().not.toExtend<EnterpriseConnectionSamlLoginHintParams>();
expectTypeOf<{ mode: 'off'; source: string }>().not.toExtend<EnterpriseConnectionSamlLoginHintParams>();
});

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<string, unknown>;

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<string, unknown>).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', () => {
Expand Down Expand Up @@ -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<string, unknown>;

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', () => {
Expand Down
65 changes: 60 additions & 5 deletions packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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. */
Expand All @@ -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;
Expand Down
Loading