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
11 changes: 11 additions & 0 deletions .changeset/major-poets-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@clerk/backend': minor
---

Align the `EnterpriseConnection` response resource with what the Backend API actually returns:

- `EnterpriseConnection` now exposes `provider`, `logoPublicUrl`, `allowOrganizationAccountLinking`, `authenticatable`, `disableJitProvisioning`, and `customAttributes`.
- `EnterpriseConnectionSamlConnection` now exposes `active`, `forceAuthn`, and `loginHint`.
- `EnterpriseConnectionOauthConfig` now exposes `providerKey`, `authUrl`, `tokenUrl`, `userInfoUrl`, and `requiresPkce`.
- Deprecated properties the Backend API never returns, which were always `undefined` despite their declared types: `allowSubdomains` on `EnterpriseConnection` (use `samlConnection.allowSubdomains`), and `idpMetadata` and `syncUserAttributes` on `EnterpriseConnectionSamlConnection` (use the top-level `syncUserAttributes`).
- `organizationId` is now normalized to `null` when the Backend API omits it, matching its declared `string | null` type. Properties backed by optional API fields (for example `oauthConfig.clientId` and the SAML IdP fields) are now typed as possibly `undefined` to match runtime behavior.
55 changes: 51 additions & 4 deletions packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,26 @@ describe('EnterpriseConnectionAPI', () => {
object: 'enterprise_connection',
id: 'entconn_123',
name: 'Clerk',
provider: 'saml_custom',
logo_public_url: 'https://img.example.com/connection-logo.png',
domains: ['clerk.dev'],
organization_id: null,
created_at: 1672531200000,
updated_at: 1672531200000,
active: true,
sync_user_attributes: false,
allow_subdomains: false,
disable_additional_identifications: false,
allow_organization_account_linking: true,
authenticatable: true,
disable_jit_provisioning: false,
custom_attributes: [
{
name: 'Employee Number',
key: 'employee_number',
sso_path: 'user.employeeNumber',
scim_path: '',
multi_valued: false,
},
],
saml_connection: {
id: 'samlc_1',
name: 'Acme SAML',
Expand All @@ -35,20 +47,29 @@ describe('EnterpriseConnectionAPI', () => {
idp_certificate_issued_at: 1672531200000,
idp_certificate_expires_at: 1704067200000,
idp_metadata_url: 'https://idp.example.com/metadata',
idp_metadata: '<xml/>',
acs_url: 'https://clerk.example.com/v1/saml/acs',
sp_entity_id: 'https://clerk.example.com',
sp_metadata_url: 'https://clerk.example.com/v1/saml/metadata',
sync_user_attributes: true,
active: true,
allow_subdomains: true,
allow_idp_initiated: false,
force_authn: false,
login_hint: {
mode: 'custom_attribute',
source: 'employee_number',
},
},
oauth_config: {
id: 'eaoc_1',
provider_key: 'custom_oidc',
name: 'Acme OIDC',
client_id: 'client_abc',
discovery_url: 'https://oauth.example.com/.well-known/openid-configuration',
auth_url: 'https://oauth.example.com/authorize',
token_url: 'https://oauth.example.com/token',
user_info_url: 'https://oauth.example.com/userinfo',
logo_public_url: 'https://img.example.com/logo.png',
requires_pkce: false,
created_at: 1672531200000,
updated_at: 1672531200000,
},
Expand Down Expand Up @@ -326,17 +347,43 @@ describe('EnterpriseConnectionAPI', () => {

expect(response.id).toBe('entconn_123');
expect(response.name).toBe('Clerk');
expect(response.provider).toBe('saml_custom');
expect(response.logoPublicUrl).toBe('https://img.example.com/connection-logo.png');
expect(response.domains).toEqual(['clerk.dev']);
expect(response.active).toBe(true);
// organization_id is omitted by the Backend API when unset and normalized to null
expect(response.organizationId).toBeNull();
expect(response.allowOrganizationAccountLinking).toBe(true);
expect(response.authenticatable).toBe(true);
expect(response.disableJitProvisioning).toBe(false);
expect(response.customAttributes).toEqual([
{
name: 'Employee Number',
key: 'employee_number',
ssoPath: 'user.employeeNumber',
scimPath: '',
multiValued: false,
},
]);
expect(response.samlConnection).not.toBeNull();
expect(response.samlConnection?.id).toBe('samlc_1');
expect(response.samlConnection?.idpEntityId).toBe('https://idp.example.com');
expect(response.samlConnection?.idpCertificateIssuedAt).toBe(1672531200000);
expect(response.samlConnection?.idpCertificateExpiresAt).toBe(1704067200000);
expect(response.samlConnection?.active).toBe(true);
expect(response.samlConnection?.forceAuthn).toBe(false);
expect(response.samlConnection?.loginHint).toEqual({
mode: 'custom_attribute',
source: 'employee_number',
});
expect(response.oauthConfig).not.toBeNull();
expect(response.oauthConfig?.providerKey).toBe('custom_oidc');
expect(response.oauthConfig?.clientId).toBe('client_abc');
expect(response.oauthConfig?.discoveryUrl).toBe('https://oauth.example.com/.well-known/openid-configuration');
expect(response.oauthConfig?.authUrl).toBe('https://oauth.example.com/authorize');
expect(response.oauthConfig?.tokenUrl).toBe('https://oauth.example.com/token');
expect(response.oauthConfig?.userInfoUrl).toBe('https://oauth.example.com/userinfo');
expect(response.oauthConfig?.requiresPkce).toBe(false);
});
});

Expand Down
153 changes: 135 additions & 18 deletions packages/backend/src/api/resources/EnterpriseConnection.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
import type {
EnterpriseConnectionCustomAttributeJSON,
EnterpriseConnectionJSON,
EnterpriseConnectionOauthConfigJSON,
EnterpriseConnectionSamlConnectionJSON,
EnterpriseConnectionSamlConnectionLoginHintJSON,
} from './JSON';

/**
* The `login_hint` configuration included on a Backend API {@link EnterpriseConnectionSamlConnection} response.
*/
export class EnterpriseConnectionSamlConnectionLoginHint {
constructor(
/** How the SAML connection emits the `login_hint` sent to the IdP: `'email_address'` sends the typed identifier, `'custom_attribute'` sends the value stored at the user `publicMetadata` key named by `source`, and `'off'` omits the `login_hint`. */
readonly mode: 'email_address' | 'custom_attribute' | 'off',
/** The user `publicMetadata` key the `login_hint` value is read from. Only set when `mode` is `'custom_attribute'`. */
readonly source?: string,
) {}

static fromJSON(data: EnterpriseConnectionSamlConnectionLoginHintJSON): EnterpriseConnectionSamlConnectionLoginHint {
return new EnterpriseConnectionSamlConnectionLoginHint(data.mode, data.source);
}
}

/**
* A custom attribute mapping included on a Backend API {@link EnterpriseConnection} response.
*/
export class EnterpriseConnectionCustomAttribute {
constructor(
/** The display name of the custom attribute. */
readonly name: string,
/** The key the custom attribute is stored under. */
readonly key: string,
/** The SSO (SAML or OIDC) attribute path the value is read from. */
readonly ssoPath: string,
/** The SCIM attribute path the value is read from. */
readonly scimPath: string,
/** Whether the custom attribute holds multiple values. */
readonly multiValued: boolean,
) {}

static fromJSON(data: EnterpriseConnectionCustomAttributeJSON): EnterpriseConnectionCustomAttribute {
return new EnterpriseConnectionCustomAttribute(
data.name,
data.key,
data.sso_path,
data.scim_path,
data.multi_valued,
);
}
}

/**
* The Backend `EnterpriseConnectionSamlConnection` object holds information about a SAML enterprise connection for an instance or organization.
*/
Expand All @@ -14,31 +60,43 @@ export class EnterpriseConnectionSamlConnection {
/** The name to use as a label for the connection. */
readonly name: string,
/** The Entity ID as provided by the Identity Provider (IdP). */
readonly idpEntityId: string,
readonly idpEntityId: string | undefined,
/** The Single-Sign On URL as provided by the Identity Provider (IdP). */
readonly idpSsoUrl: string,
readonly idpSsoUrl: string | undefined,
/** The X.509 certificate as provided by the Identity Provider (IdP). */
readonly idpCertificate: string,
readonly idpCertificate: string | undefined,
/** The Unix timestamp when the Identity Provider (IdP) certificate was issued. */
readonly idpCertificateIssuedAt: number,
readonly idpCertificateIssuedAt: number | undefined,
/** The Unix timestamp when the Identity Provider (IdP) certificate expires. */
readonly idpCertificateExpiresAt: number,
readonly idpCertificateExpiresAt: number | undefined,
/** The URL which serves the Identity Provider (IdP) metadata. */
readonly idpMetadataUrl: string,
/** The XML content of the Identity Provider (IdP) metadata file. */
readonly idpMetadata: string,
readonly idpMetadataUrl: string | undefined,
/**
* The XML content of the Identity Provider (IdP) metadata file.
* @deprecated The Backend API does not return this field, so it is always `undefined`.
*/
readonly idpMetadata: string | undefined,
/** The Assertion Consumer Service (ACS) URL of the connection. */
readonly acsUrl: string,
readonly acsUrl: string | undefined,
/** The Entity ID as provided by the Service Provider (Clerk). */
readonly spEntityId: string,
readonly spEntityId: string | undefined,
/** The metadata URL as provided by the Service Provider (Clerk). */
readonly spMetadataUrl: string,
/** Whether the connection syncs user attributes between the IdP and Clerk. */
readonly syncUserAttributes: boolean,
readonly spMetadataUrl: string | undefined,
/**
* Whether the connection syncs user attributes between the IdP and Clerk.
* @deprecated The Backend API does not return this field on the nested SAML connection, so it is always `undefined`. Use the top-level `syncUserAttributes` on {@link EnterpriseConnection} instead.
*/
readonly syncUserAttributes: boolean | undefined,
/** Whether users with an email address subdomain are allowed to use this connection. */
readonly allowSubdomains: boolean,
/** Whether IdP-initiated SSO is allowed. */
readonly allowIdpInitiated: boolean,
/** Whether the SAML connection is active. */
readonly active: boolean,
/** Whether the SAML connection requires force authentication. */
readonly forceAuthn: boolean,
/** The `login_hint` configuration of the SAML connection. */
readonly loginHint: EnterpriseConnectionSamlConnectionLoginHint,
) {}

static fromJSON(data: EnterpriseConnectionSamlConnectionJSON): EnterpriseConnectionSamlConnection {
Expand All @@ -58,6 +116,9 @@ export class EnterpriseConnectionSamlConnection {
data.sync_user_attributes,
data.allow_subdomains,
data.allow_idp_initiated,
data.active,
data.force_authn,
EnterpriseConnectionSamlConnectionLoginHint.fromJSON(data.login_hint),
);
}
}
Expand All @@ -78,15 +139,15 @@ export class EnterpriseConnectionOauthConfig {
/**
* The OAuth client ID.
*/
readonly clientId: string,
readonly clientId: string | undefined,
/**
* The OpenID Connect discovery URL.
*/
readonly discoveryUrl: string,
readonly discoveryUrl: string | undefined,
/**
* The public URL of the OAuth provider logo, if available.
*/
readonly logoPublicUrl: string,
readonly logoPublicUrl: string | undefined,
/**
* The date when the configuration was first created.
*/
Expand All @@ -95,6 +156,26 @@ export class EnterpriseConnectionOauthConfig {
* The date when the configuration was last updated.
*/
readonly updatedAt: number,
/**
* The OAuth provider key of the configuration. For example, `'custom_oidc'`.
*/
readonly providerKey: string,
/**
* The OAuth authorization URL.
*/
readonly authUrl: string | undefined,
/**
* The OAuth token URL.
*/
readonly tokenUrl: string | undefined,
/**
* The OAuth user info URL.
*/
readonly userInfoUrl: string | undefined,
/**
* Whether the OAuth configuration requires PKCE.
*/
readonly requiresPkce: boolean,
) {}

static fromJSON(data: EnterpriseConnectionOauthConfigJSON): EnterpriseConnectionOauthConfig {
Expand All @@ -106,6 +187,11 @@ export class EnterpriseConnectionOauthConfig {
data.logo_public_url,
data.created_at,
data.updated_at,
data.provider_key,
data.auth_url,
data.token_url,
data.user_info_url,
data.requires_pkce,
);
}
}
Expand Down Expand Up @@ -141,8 +227,9 @@ export class EnterpriseConnection {
readonly syncUserAttributes: boolean,
/**
* Whether users with an email address subdomain are allowed to use this connection or not.
* @deprecated The Backend API does not return this field at the top level, so it is always `undefined`. Use `samlConnection.allowSubdomains` instead.
*/
readonly allowSubdomains: boolean,
readonly allowSubdomains: boolean | undefined,
/**
* Whether additional identifications are disabled for this connection.
*/
Expand All @@ -163,14 +250,38 @@ export class EnterpriseConnection {
* OAuth (OIDC) configuration when the enterprise connection uses OAuth.
*/
readonly oauthConfig: EnterpriseConnectionOauthConfig | null,
/**
* The identity provider (IdP) of the connection. For example, `'saml_custom'` or `'oidc_custom'`.
*/
readonly provider: string,
/**
* The public URL of the provider logo, if available.
*/
readonly logoPublicUrl: string | undefined,
/**
* Whether existing users who are members of the organization can link their account to their enterprise identity.
*/
readonly allowOrganizationAccountLinking: boolean,
/**
* Whether the connection can be used for sign-in and sign-up.
*/
readonly authenticatable: boolean,
/**
* Whether Just-in-Time (JIT) provisioning of users is disabled for the connection.
*/
readonly disableJitProvisioning: boolean,
/**
* The custom attribute mappings of the connection. Only returned when the custom attributes feature is enabled for the instance.
*/
readonly customAttributes: EnterpriseConnectionCustomAttribute[] | undefined,
) {}

static fromJSON(data: EnterpriseConnectionJSON): EnterpriseConnection {
return new EnterpriseConnection(
data.id,
data.name,
data.domains,
data.organization_id,
data.organization_id ?? null,
data.active,
data.sync_user_attributes,
data.allow_subdomains,
Expand All @@ -179,6 +290,12 @@ export class EnterpriseConnection {
data.updated_at,
data.saml_connection != null ? EnterpriseConnectionSamlConnection.fromJSON(data.saml_connection) : null,
data.oauth_config != null ? EnterpriseConnectionOauthConfig.fromJSON(data.oauth_config) : null,
data.provider,
data.logo_public_url,
data.allow_organization_account_linking,
data.authenticatable,
data.disable_jit_provisioning,
data.custom_attributes?.map(attr => EnterpriseConnectionCustomAttribute.fromJSON(attr)),
);
}
}
Loading
Loading