diff --git a/.changeset/little-lights-press.md b/.changeset/little-lights-press.md new file mode 100644 index 00000000000..5735ce8c1bb --- /dev/null +++ b/.changeset/little-lights-press.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/backend': minor +'@clerk/shared': minor +--- + +Add `idpCertificateIssuedAt` and `idpCertificateExpiresAt` to SAML enterprise connections, exposing the IdP certificate validity window diff --git a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts index 40033c3635c..739eeb6445c 100644 --- a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts +++ b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts @@ -28,6 +28,8 @@ describe('EnterpriseConnectionAPI', () => { idp_entity_id: 'https://idp.example.com', idp_sso_url: 'https://idp.example.com/sso', idp_certificate: '-----BEGIN CERTIFICATE-----', + idp_certificate_issued_at: 1672531200000, + idp_certificate_expires_at: 1704067200000, idp_metadata_url: 'https://idp.example.com/metadata', idp_metadata: '', acs_url: 'https://clerk.example.com/v1/saml/acs', @@ -205,6 +207,8 @@ describe('EnterpriseConnectionAPI', () => { 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.oauthConfig).not.toBeNull(); expect(response.oauthConfig?.clientId).toBe('client_abc'); expect(response.oauthConfig?.discoveryUrl).toBe('https://oauth.example.com/.well-known/openid-configuration'); diff --git a/packages/backend/src/api/__tests__/SamlConnectionApi.test.ts b/packages/backend/src/api/__tests__/SamlConnectionApi.test.ts index f2832468a7b..fcc5f550578 100644 --- a/packages/backend/src/api/__tests__/SamlConnectionApi.test.ts +++ b/packages/backend/src/api/__tests__/SamlConnectionApi.test.ts @@ -26,6 +26,8 @@ describe('SamlConnectionAPI', () => { idp_entity_id: 'entity_123', idp_sso_url: 'https://idp.example.com/sso', idp_certificate: 'cert_data', + idp_certificate_issued_at: 1672531200000, + idp_certificate_expires_at: 1704067200000, idp_metadata_url: null, idp_metadata: null, attribute_mapping: { @@ -70,6 +72,8 @@ describe('SamlConnectionAPI', () => { expect(response.data[0].id).toBe('samlc_123'); expect(response.data[0].name).toBe('Test Connection'); expect(response.data[0].organizationId).toBe('org_123'); + expect(response.data[0].idpCertificateIssuedAt).toBe(1672531200000); + expect(response.data[0].idpCertificateExpiresAt).toBe(1704067200000); expect(response.totalCount).toBe(1); }); }); @@ -114,6 +118,8 @@ describe('SamlConnectionAPI', () => { expect(response.id).toBe('samlc_123'); expect(response.name).toBe('Test Connection'); expect(response.organizationId).toBe('org_123'); + expect(response.idpCertificateIssuedAt).toBe(1672531200000); + expect(response.idpCertificateExpiresAt).toBe(1704067200000); }); }); diff --git a/packages/backend/src/api/resources/EnterpriseConnection.ts b/packages/backend/src/api/resources/EnterpriseConnection.ts index 5eaf37e8beb..49c6362d635 100644 --- a/packages/backend/src/api/resources/EnterpriseConnection.ts +++ b/packages/backend/src/api/resources/EnterpriseConnection.ts @@ -19,6 +19,10 @@ export class EnterpriseConnectionSamlConnection { readonly idpSsoUrl: string, /** The X.509 certificate as provided by the Identity Provider (IdP). */ readonly idpCertificate: string, + /** The Unix timestamp when the Identity Provider (IdP) certificate was issued. */ + readonly idpCertificateIssuedAt: number, + /** The Unix timestamp when the Identity Provider (IdP) certificate expires. */ + readonly idpCertificateExpiresAt: number, /** The URL which serves the Identity Provider (IdP) metadata. */ readonly idpMetadataUrl: string, /** The XML content of the Identity Provider (IdP) metadata file. */ @@ -44,6 +48,8 @@ export class EnterpriseConnectionSamlConnection { data.idp_entity_id, data.idp_sso_url, data.idp_certificate, + data.idp_certificate_issued_at, + data.idp_certificate_expires_at, data.idp_metadata_url, data.idp_metadata, data.acs_url, diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 27989fd3036..298c4983d0a 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -758,6 +758,8 @@ export interface EnterpriseConnectionSamlConnectionJSON { idp_entity_id: string; idp_sso_url: string; idp_certificate: string; + idp_certificate_issued_at: number; + idp_certificate_expires_at: number; idp_metadata_url: string; idp_metadata: string; acs_url: string; @@ -801,6 +803,8 @@ export interface SamlConnectionJSON extends ClerkResourceJSON { idp_entity_id: string; idp_sso_url: string; idp_certificate: string; + idp_certificate_issued_at: number; + idp_certificate_expires_at: number; idp_metadata_url: string; idp_metadata: string; acs_url: string; diff --git a/packages/backend/src/api/resources/SamlConnection.ts b/packages/backend/src/api/resources/SamlConnection.ts index 5f58b9210a0..60cab377337 100644 --- a/packages/backend/src/api/resources/SamlConnection.ts +++ b/packages/backend/src/api/resources/SamlConnection.ts @@ -20,6 +20,10 @@ export class SamlConnection { readonly idpSsoUrl: string | null, /** The X.509 certificate as provided by the Identity Provider (IdP). */ readonly idpCertificate: string | null, + /** The Unix timestamp when the Identity Provider (IdP) certificate was issued. */ + readonly idpCertificateIssuedAt: number, + /** The Unix timestamp when the Identity Provider (IdP) certificate expires. */ + readonly idpCertificateExpiresAt: number, /** The URL which serves the Identity Provider (IdP) metadata. If present, it takes priority over the corresponding individual properties. */ readonly idpMetadataUrl: string | null, /** The XML content of the Identity Provider (IdP) metadata file. If present, it takes priority over the corresponding individual properties. */ @@ -58,6 +62,8 @@ export class SamlConnection { data.idp_entity_id, data.idp_sso_url, data.idp_certificate, + data.idp_certificate_issued_at, + data.idp_certificate_expires_at, data.idp_metadata_url, data.idp_metadata, data.acs_url, diff --git a/packages/clerk-js/src/core/resources/EnterpriseConnection.ts b/packages/clerk-js/src/core/resources/EnterpriseConnection.ts index 239f401822a..109e7775ce1 100644 --- a/packages/clerk-js/src/core/resources/EnterpriseConnection.ts +++ b/packages/clerk-js/src/core/resources/EnterpriseConnection.ts @@ -19,6 +19,8 @@ function samlNestedFromJSON(data: EnterpriseSamlConnectionNestedJSON): Enterpris idpEntityId: data.idp_entity_id, idpSsoUrl: data.idp_sso_url, idpCertificate: data.idp_certificate, + idpCertificateIssuedAt: data.idp_certificate_issued_at, + idpCertificateExpiresAt: data.idp_certificate_expires_at, idpMetadataUrl: data.idp_metadata_url, idpMetadata: data.idp_metadata, acsUrl: data.acs_url, @@ -38,6 +40,8 @@ function samlNestedToJSON(data: EnterpriseSamlConnectionNestedResource): Enterpr idp_entity_id: data.idpEntityId, idp_sso_url: data.idpSsoUrl, idp_certificate: data.idpCertificate, + idp_certificate_issued_at: data.idpCertificateIssuedAt, + idp_certificate_expires_at: data.idpCertificateExpiresAt, idp_metadata_url: data.idpMetadataUrl, idp_metadata: data.idpMetadata, acs_url: data.acsUrl, diff --git a/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts b/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts index bc117ba562a..351629f9f6c 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts @@ -88,6 +88,8 @@ describe('Organization', () => { idp_entity_id: 'https://idp.acme.com/entity', idp_sso_url: 'https://idp.acme.com/sso', idp_certificate: 'MIICertificatePlaceholder', + idp_certificate_issued_at: 1672531200000, + idp_certificate_expires_at: 1704067200000, idp_metadata_url: 'https://idp.acme.com/metadata', idp_metadata: '', acs_url: 'https://clerk.example.com/v1/saml/acs', diff --git a/packages/clerk-js/src/core/resources/__tests__/User.test.ts b/packages/clerk-js/src/core/resources/__tests__/User.test.ts index d717c6ba420..ae380cce8aa 100644 --- a/packages/clerk-js/src/core/resources/__tests__/User.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/User.test.ts @@ -102,6 +102,8 @@ describe('User', () => { idp_entity_id: 'https://idp.acme.com/entity', idp_sso_url: 'https://idp.acme.com/sso', idp_certificate: 'MIICertificatePlaceholder', + idp_certificate_issued_at: 1672531200000, + idp_certificate_expires_at: 1704067200000, idp_metadata_url: 'https://idp.acme.com/metadata', idp_metadata: '', acs_url: 'https://clerk.example.com/v1/saml/acs', diff --git a/packages/shared/src/types/enterpriseConnection.ts b/packages/shared/src/types/enterpriseConnection.ts index 0a5636e14d4..35a3f8a336f 100644 --- a/packages/shared/src/types/enterpriseConnection.ts +++ b/packages/shared/src/types/enterpriseConnection.ts @@ -22,20 +22,35 @@ export interface EnterpriseConnectionJSON extends ClerkResourceJSON { export type EnterpriseConnectionJSONSnapshot = EnterpriseConnectionJSON; export interface EnterpriseConnectionResource extends ClerkResource { + /** The enterprise connection ID. */ id: string; + /** The display name of the connection. */ name: string; + /** Whether the enterprise connection is active. */ active: boolean; + /** The identity provider the connection uses (e.g. `saml_okta`, `oidc_custom`). */ provider: string; + /** The public URL of the identity provider's logo, or `null` when none is set. */ logoPublicUrl: string | null; + /** Domains associated with the enterprise connection. */ domains: string[]; + /** Organization ID when the connection is linked to an organization, otherwise `null`. */ organizationId: string | null; + /** Whether user attributes are synced from the identity provider on each sign-in. */ syncUserAttributes: boolean; + /** Whether additional identification methods are disabled for users signing in through this connection. */ disableAdditionalIdentifications: boolean; + /** Whether this connection supports account linking via organization membership. */ allowOrganizationAccountLinking: boolean; + /** Custom attributes configured for the connection. */ customAttributes: unknown[]; + /** The OAuth configuration, present when the enterprise connection uses OIDC, otherwise `null`. */ oauthConfig: EnterpriseOAuthConfigResource | null; + /** The SAML configuration, present when the enterprise connection uses SAML, otherwise `null`. */ samlConnection: EnterpriseSamlConnectionNestedResource | null; + /** The date when the connection was created. */ createdAt: Date | null; + /** The date when the connection was last updated. */ updatedAt: Date | null; __internal_toSnapshot: () => EnterpriseConnectionJSONSnapshot; } @@ -47,6 +62,8 @@ export interface EnterpriseSamlConnectionNestedJSON { idp_entity_id: string; idp_sso_url: string; idp_certificate: string; + idp_certificate_issued_at: number; + idp_certificate_expires_at: number; idp_metadata_url: string; idp_metadata: string; acs_url: string; @@ -64,6 +81,10 @@ export interface EnterpriseSamlConnectionNestedResource { idpEntityId: string; idpSsoUrl: string; idpCertificate: string; + /** Unix timestamp (milliseconds) of the start of the IdP certificate validity window (X.509 NotBefore). */ + idpCertificateIssuedAt: number; + /** Unix timestamp (milliseconds) of the end of the IdP certificate validity window (X.509 NotAfter). */ + idpCertificateExpiresAt: number; idpMetadataUrl: string; idpMetadata: string; acsUrl: string;