Skip to content
Draft
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
15 changes: 15 additions & 0 deletions docs/resource-specific-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2034,6 +2034,19 @@ 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 `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).

### Workflow

To add or rotate a credential:
Expand All @@ -2055,6 +2068,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...
Expand Down
37 changes: 31 additions & 6 deletions src/tools/auth0/handlers/clientAuthCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,16 @@ 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;
}[] = [];

if (client.client_authentication_methods) {
for (const [methodKey, methodVal] of Object.entries(
Expand All @@ -94,6 +103,10 @@ 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,
});
}
}
Expand Down Expand Up @@ -139,11 +152,23 @@ export default class ClientAuthCredentialsHandler {
const createdIdByName = new Map<string, string>();
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. `!= 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,
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,
}).filter(([, v]) => v != null)
);
const created = await (this.client.clients.credentials.create as Function)(
clientId,
createPayload
);
log.info(
`clientAuthCredentials: created credential "${cred.name}" on client "${clientName}"`
);
Expand Down
59 changes: 59 additions & 0 deletions test/tools/auth0/handlers/clientAuthCredentials.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,65 @@ describe('#clientAuthCredentials handler', () => {
).to.deep.equal([{ id: 'cred_new123' }]);
});

it('should forward kid and other optional fields to create, omitting null/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,
// null (e.g. an empty `expires_at:` in YAML) must be dropped, not sent
expires_at: null,
},
],
},
},
},
],
},
]);

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);
// null and undefined optional fields must not be sent (Auth0 rejects nulls)
expect(data).to.not.have.property('expires_at');
});

it('should resolve client_id by name when client_id is null (directory mode)', async () => {
const createCalls: any[] = [];

Expand Down