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/heavy-melons-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': patch
---

Add the required `provider` field to `CreateEnterpriseConnectionParams`. The Backend API has always required `provider` when creating an enterprise connection, so calls to `createEnterpriseConnection()` without it type-checked but failed at runtime. The field is typed to the supported provider values (`'saml_custom'`, `'saml_okta'`, `'saml_google'`, `'saml_microsoft'`, `'oidc_custom'`, `'oidc_github_enterprise'`, `'oidc_gitlab'`), so unsupported values are also caught at compile time.
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.
3 changes: 1 addition & 2 deletions integration/tests/tanstack-start/enterprise-sso.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const FAKE_IDP_CERTIFICATE =
/**
* Helper to create and activate a SAML enterprise connection.
* The Clerk API requires creating the connection first (inactive), then activating via update.
* The `provider` field is required by the API but missing from the SDK types, so we cast.
*/
async function createActiveEnterpriseConnection(
clerk: ReturnType<typeof createTestUtils>['services']['clerk'],
Expand All @@ -28,7 +27,7 @@ async function createActiveEnterpriseConnection(
idpSsoUrl: opts.idpSsoUrl,
idpCertificate: FAKE_IDP_CERTIFICATE,
},
} as Parameters<typeof clerk.enterpriseConnections.createEnterpriseConnection>[0]);
});

return clerk.enterpriseConnections.updateEnterpriseConnection(conn.id, { active: true });
}
Expand Down
127 changes: 126 additions & 1 deletion packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { http, HttpResponse } from 'msw';
import { describe, expect, it } from 'vitest';
import { describe, expect, expectTypeOf, it } from 'vitest';

import { server, validateHeaders } from '../../mock-server';
import type {
CreateEnterpriseConnectionParams,
EnterpriseConnectionSamlLoginHintParams,
} from '../endpoints/EnterpriseConnectionApi';
import { createBackendApiClient } from '../factory';

describe('EnterpriseConnectionAPI', () => {
Expand Down Expand Up @@ -60,6 +64,7 @@ describe('EnterpriseConnectionAPI', () => {

expect(body.name).toBe('Clerk');
expect(body.domains).toEqual(['clerk.dev']);
expect(body.provider).toBe('saml_custom');
expect(body.saml).toEqual({
idp_entity_id: 'xxx',
idp_metadata_url: 'https://oauth.devsuccess.app/metadata',
Expand All @@ -74,6 +79,7 @@ describe('EnterpriseConnectionAPI', () => {
await apiClient.enterpriseConnections.createEnterpriseConnection({
name: 'Clerk',
domains: ['clerk.dev'],
provider: 'saml_custom',
saml: {
idpEntityId: 'xxx',
idpMetadataUrl: 'https://oauth.devsuccess.app/metadata',
Expand All @@ -89,6 +95,7 @@ describe('EnterpriseConnectionAPI', () => {
validateHeaders(async ({ request }) => {
const body = (await request.json()) as Record<string, unknown>;

expect(body.provider).toBe('oidc_custom');
expect(body.oidc).toEqual({
discovery_url: 'https://oidc.example.com/.well-known/openid-configuration',
client_id: 'client_123',
Expand All @@ -107,6 +114,7 @@ describe('EnterpriseConnectionAPI', () => {
await apiClient.enterpriseConnections.createEnterpriseConnection({
name: 'OIDC Connection',
domains: ['example.com'],
provider: 'oidc_custom',
oidc: {
discoveryUrl: 'https://oidc.example.com/.well-known/openid-configuration',
clientId: 'client_123',
Expand All @@ -118,6 +126,82 @@ describe('EnterpriseConnectionAPI', () => {
},
});
});

it('requires provider and rejects unsupported values at the type level', () => {
expectTypeOf<{ name: string; domains: string[] }>().not.toExtend<CreateEnterpriseConnectionParams>();
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 @@ -146,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
69 changes: 63 additions & 6 deletions packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClerkPaginationRequest } from '@clerk/shared/types';
import type { ClerkPaginationRequest, OrganizationEnterpriseConnectionProvider } from '@clerk/shared/types';

import { joinPaths } from '../../util/path';
import type { EnterpriseConnection } from '../resources';
Expand Down 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,20 +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 @@ -101,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