From 8d4146b151102129168a60e0645a31a44f90ba4e Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Thu, 10 Sep 2026 10:52:20 +0530 Subject: [PATCH 1/3] fix(clients): honor kid and other credential fields on private_key_jwt/mTLS credential creation --- docs/resource-specific-documentation.md | 16 +++++ .../auth0/handlers/clientAuthCredentials.ts | 42 ++++++++++++-- .../handlers/clientAuthCredentials.tests.ts | 58 +++++++++++++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 622348158..806317329 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -2034,6 +2034,20 @@ The Deploy CLI supports managing client authentication credentials for Private K | `x509_cert` | `self_signed_tls_client_auth` | mTLS (self-signed cert) | | `cert_subject_dn` | `tls_client_auth` | mTLS (CA-signed cert, subject DN) | +### Optional credential fields + +Beyond `name`, `credential_type`, and `pem`, the following optional fields are forwarded to Auth0 when a credential is created (each applies only to certain credential types — see the [Management API docs](https://auth0.com/docs/api/management/v2/clients/post-credentials)): + +| Field | Applies to | Notes | +| ------------------------ | ----------------- | ---------------------------------------------------------------------------- | +| `kid` | `public_key` | Key ID. If omitted, Auth0 auto-generates one. Format: `[0-9a-zA-Z-_]{10,64}` | +| `alg` | `public_key` | Signing algorithm: `RS256`, `RS384`, or `PS256` | +| `expires_at` | `public_key` | ISO 8601 expiry. If omitted, the credential never expires | +| `parse_expiry_from_cert` | `public_key` | Parse expiry from the provided X509 PEM | +| `subject_dn` | `cert_subject_dn` | Subject Distinguished Name. Mutually exclusive with `pem` | + +> **Note:** These fields are honored **only when the credential is created** (matching is by `name`). Changing a field such as `kid` on an existing credential with the same `name` is a no-op — rotate by adding a new credential under a new `name` and removing the old one. `kid` is not exported (Auth0 returns only `name` and `credential_type` on read). + ### Workflow To add or rotate a credential: @@ -2055,6 +2069,8 @@ To add or rotate a credential: credentials: - name: my-key-v2 credential_type: public_key + kid: my-custom-kid # optional; auto-generated if omitted + alg: RS256 # optional pem: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkq... diff --git a/src/tools/auth0/handlers/clientAuthCredentials.ts b/src/tools/auth0/handlers/clientAuthCredentials.ts index a73bbd7d1..2e2096788 100644 --- a/src/tools/auth0/handlers/clientAuthCredentials.ts +++ b/src/tools/auth0/handlers/clientAuthCredentials.ts @@ -80,7 +80,17 @@ export default class ClientAuthCredentialsHandler { const clientName = client.name || clientId; // Collect all desired credentials across all auth methods (only pem-bearing entries) - const desired: { name: string; pem?: string; credential_type: string; method: string }[] = []; + const desired: { + name: string; + pem?: string; + credential_type: string; + method: string; + kid?: string; + alg?: string; + expires_at?: string; + parse_expiry_from_cert?: boolean; + subject_dn?: string; + }[] = []; if (client.client_authentication_methods) { for (const [methodKey, methodVal] of Object.entries( @@ -94,6 +104,11 @@ export default class ClientAuthCredentialsHandler { pem: cred.pem, credential_type: cred.credential_type || this.inferCredentialType(methodKey), method: methodKey, + kid: cred.kid, + alg: cred.alg, + expires_at: cred.expires_at, + parse_expiry_from_cert: cred.parse_expiry_from_cert, + subject_dn: cred.subject_dn, }); } } @@ -139,11 +154,26 @@ export default class ClientAuthCredentialsHandler { const createdIdByName = new Map(); for (const cred of toCreate) { try { - const created = await (this.client.clients.credentials.create as Function)(clientId, { - name: cred.name, - pem: cred.pem, - credential_type: cred.credential_type, - }); + // Forward all API-accepted fields; drop undefined ones so we never send + // nulls the Management API rejects. kid/alg/expires_at/parse_expiry_from_cert/ + // subject_dn are optional per credential_type — if omitted, Auth0 defaults them + // (e.g. auto-generates a kid). + const createPayload = Object.fromEntries( + Object.entries({ + name: cred.name, + pem: cred.pem, + credential_type: cred.credential_type, + kid: cred.kid, + alg: cred.alg, + expires_at: cred.expires_at, + parse_expiry_from_cert: cred.parse_expiry_from_cert, + subject_dn: cred.subject_dn, + }).filter(([, v]) => v !== undefined) + ); + const created = await (this.client.clients.credentials.create as Function)( + clientId, + createPayload + ); log.info( `clientAuthCredentials: created credential "${cred.name}" on client "${clientName}"` ); diff --git a/test/tools/auth0/handlers/clientAuthCredentials.tests.ts b/test/tools/auth0/handlers/clientAuthCredentials.tests.ts index 7b372b375..20d8754dc 100644 --- a/test/tools/auth0/handlers/clientAuthCredentials.tests.ts +++ b/test/tools/auth0/handlers/clientAuthCredentials.tests.ts @@ -200,6 +200,64 @@ describe('#clientAuthCredentials handler', () => { ).to.deep.equal([{ id: 'cred_new123' }]); }); + it('should forward kid and other optional fields to create, omitting undefined ones', async () => { + const createCalls: any[] = []; + + const client = makeClient({ + clients: { + list: (params) => + mockPagedData(params, 'clients', [{ client_id: 'client1', name: 'My App' }]), + update: () => Promise.resolve({ data: {} }), + credentials: { + list: () => Promise.resolve([]), + create: (clientId, data) => { + createCalls.push({ clientId, data }); + return Promise.resolve({ id: 'cred_new123', name: data.name }); + }, + delete: () => Promise.resolve({}), + }, + }, + }); + + const handler = new clientAuthCredentials({ client, config: makeConfig() }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + await stageFn.apply(handler, [ + { + clients: [ + { + client_id: 'client1', + name: 'My App', + client_authentication_methods: { + private_key_jwt: { + credentials: [ + { + name: 'new-key', + pem: '-----BEGIN PUBLIC KEY-----\nabc\n-----END PUBLIC KEY-----\n', + credential_type: 'public_key', + kid: 'MY_CUSTOM_KID_VALUE', + alg: 'RS256', + // explicit false must be forwarded, not dropped as falsy + parse_expiry_from_cert: false, + }, + ], + }, + }, + }, + ], + }, + ]); + + expect(createCalls).to.have.lengthOf(1); + const { data } = createCalls[0]; + expect(data.kid).to.equal('MY_CUSTOM_KID_VALUE'); + expect(data.alg).to.equal('RS256'); + // explicit false is a valid value and must be sent, not filtered out + expect(data).to.have.property('parse_expiry_from_cert', false); + // undefined optional fields must not be sent (Auth0 rejects nulls) + expect(data).to.not.have.property('expires_at'); + expect(data).to.not.have.property('subject_dn'); + }); + it('should resolve client_id by name when client_id is null (directory mode)', async () => { const createCalls: any[] = []; From b0ecc081e6329492caae1b69221c99cc318ae287 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Thu, 10 Sep 2026 11:33:23 +0530 Subject: [PATCH 2/3] fix(clients): honor kid and other credential fields on private_key_jwt/mTLS credential creation --- docs/resource-specific-documentation.md | 17 ++++++++--------- .../auth0/handlers/clientAuthCredentials.ts | 14 ++++++-------- .../handlers/clientAuthCredentials.tests.ts | 7 ++++--- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 806317329..bba2b5489 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -2036,15 +2036,14 @@ The Deploy CLI supports managing client authentication credentials for Private K ### Optional credential fields -Beyond `name`, `credential_type`, and `pem`, the following optional fields are forwarded to Auth0 when a credential is created (each applies only to certain credential types — see the [Management API docs](https://auth0.com/docs/api/management/v2/clients/post-credentials)): - -| Field | Applies to | Notes | -| ------------------------ | ----------------- | ---------------------------------------------------------------------------- | -| `kid` | `public_key` | Key ID. If omitted, Auth0 auto-generates one. Format: `[0-9a-zA-Z-_]{10,64}` | -| `alg` | `public_key` | Signing algorithm: `RS256`, `RS384`, or `PS256` | -| `expires_at` | `public_key` | ISO 8601 expiry. If omitted, the credential never expires | -| `parse_expiry_from_cert` | `public_key` | Parse expiry from the provided X509 PEM | -| `subject_dn` | `cert_subject_dn` | Subject Distinguished Name. Mutually exclusive with `pem` | +Beyond `name`, `credential_type`, and `pem`, the following optional fields are forwarded to Auth0 when a `public_key` (`private_key_jwt`) credential is created (see the [Management API docs](https://auth0.com/docs/api/management/v2/clients/post-credentials)): + +| Field | Notes | +| ------------------------ | ---------------------------------------------------------------------------- | +| `kid` | Key ID. If omitted, Auth0 auto-generates one. Format: `[0-9a-zA-Z-_]{10,64}` | +| `alg` | Signing algorithm: `RS256`, `RS384`, or `PS256` | +| `expires_at` | ISO 8601 expiry. If omitted, the credential never expires | +| `parse_expiry_from_cert` | Parse the expiry from the X509 certificate supplied in `pem` | > **Note:** These fields are honored **only when the credential is created** (matching is by `name`). Changing a field such as `kid` on an existing credential with the same `name` is a no-op — rotate by adding a new credential under a new `name` and removing the old one. `kid` is not exported (Auth0 returns only `name` and `credential_type` on read). diff --git a/src/tools/auth0/handlers/clientAuthCredentials.ts b/src/tools/auth0/handlers/clientAuthCredentials.ts index 2e2096788..f88335244 100644 --- a/src/tools/auth0/handlers/clientAuthCredentials.ts +++ b/src/tools/auth0/handlers/clientAuthCredentials.ts @@ -89,7 +89,6 @@ export default class ClientAuthCredentialsHandler { alg?: string; expires_at?: string; parse_expiry_from_cert?: boolean; - subject_dn?: string; }[] = []; if (client.client_authentication_methods) { @@ -108,7 +107,6 @@ export default class ClientAuthCredentialsHandler { alg: cred.alg, expires_at: cred.expires_at, parse_expiry_from_cert: cred.parse_expiry_from_cert, - subject_dn: cred.subject_dn, }); } } @@ -154,10 +152,11 @@ export default class ClientAuthCredentialsHandler { const createdIdByName = new Map(); for (const cred of toCreate) { try { - // Forward all API-accepted fields; drop undefined ones so we never send - // nulls the Management API rejects. kid/alg/expires_at/parse_expiry_from_cert/ - // subject_dn are optional per credential_type — if omitted, Auth0 defaults them - // (e.g. auto-generates a kid). + // Forward all API-accepted fields; drop null/undefined ones so we never send + // nulls the Management API rejects (an empty YAML value such as `kid:` parses to + // null). kid/alg/expires_at/parse_expiry_from_cert are optional — if omitted, + // Auth0 defaults them (e.g. auto-generates a kid). Loose `!= null` is intentional: + // it drops null and undefined but keeps an explicit `false`. const createPayload = Object.fromEntries( Object.entries({ name: cred.name, @@ -167,8 +166,7 @@ export default class ClientAuthCredentialsHandler { alg: cred.alg, expires_at: cred.expires_at, parse_expiry_from_cert: cred.parse_expiry_from_cert, - subject_dn: cred.subject_dn, - }).filter(([, v]) => v !== undefined) + }).filter(([, v]) => v != null) ); const created = await (this.client.clients.credentials.create as Function)( clientId, diff --git a/test/tools/auth0/handlers/clientAuthCredentials.tests.ts b/test/tools/auth0/handlers/clientAuthCredentials.tests.ts index 20d8754dc..592e3849d 100644 --- a/test/tools/auth0/handlers/clientAuthCredentials.tests.ts +++ b/test/tools/auth0/handlers/clientAuthCredentials.tests.ts @@ -200,7 +200,7 @@ describe('#clientAuthCredentials handler', () => { ).to.deep.equal([{ id: 'cred_new123' }]); }); - it('should forward kid and other optional fields to create, omitting undefined ones', async () => { + it('should forward kid and other optional fields to create, omitting null/undefined ones', async () => { const createCalls: any[] = []; const client = makeClient({ @@ -238,6 +238,8 @@ describe('#clientAuthCredentials handler', () => { alg: 'RS256', // explicit false must be forwarded, not dropped as falsy parse_expiry_from_cert: false, + // null (e.g. an empty `expires_at:` in YAML) must be dropped, not sent + expires_at: null, }, ], }, @@ -253,9 +255,8 @@ describe('#clientAuthCredentials handler', () => { expect(data.alg).to.equal('RS256'); // explicit false is a valid value and must be sent, not filtered out expect(data).to.have.property('parse_expiry_from_cert', false); - // undefined optional fields must not be sent (Auth0 rejects nulls) + // null and undefined optional fields must not be sent (Auth0 rejects nulls) expect(data).to.not.have.property('expires_at'); - expect(data).to.not.have.property('subject_dn'); }); it('should resolve client_id by name when client_id is null (directory mode)', async () => { From d892f732da50f439fdc3919e00ab1d6b32d5b4f2 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Thu, 10 Sep 2026 11:35:28 +0530 Subject: [PATCH 3/3] fix(clients): honor kid and other credential fields on private_key_jwt/mTLS credential creation --- src/tools/auth0/handlers/clientAuthCredentials.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/tools/auth0/handlers/clientAuthCredentials.ts b/src/tools/auth0/handlers/clientAuthCredentials.ts index f88335244..adb89fe3c 100644 --- a/src/tools/auth0/handlers/clientAuthCredentials.ts +++ b/src/tools/auth0/handlers/clientAuthCredentials.ts @@ -152,11 +152,8 @@ export default class ClientAuthCredentialsHandler { const createdIdByName = new Map(); for (const cred of toCreate) { try { - // Forward all API-accepted fields; drop null/undefined ones so we never send - // nulls the Management API rejects (an empty YAML value such as `kid:` parses to - // null). kid/alg/expires_at/parse_expiry_from_cert are optional — if omitted, - // Auth0 defaults them (e.g. auto-generates a kid). Loose `!= null` is intentional: - // it drops null and undefined but keeps an explicit `false`. + // Forward all API-accepted fields. `!= null` drops unset (undefined) and empty + // (YAML `kid:` → null) values the API rejects, while keeping an explicit `false`. const createPayload = Object.fromEntries( Object.entries({ name: cred.name,