From 05d6c8ee056189cbe95530d518c8b6a1f0750d5d Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 30 Jun 2025 21:04:49 -0700 Subject: [PATCH 01/51] chore(backend): Introduce machine token secrets as authorization header --- .../src/api/endpoints/MachineTokensApi.ts | 85 +++++++++++++++++++ packages/backend/src/api/request.ts | 2 +- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 4c61f35d235..75887a5841b 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -1,10 +1,95 @@ import { joinPaths } from '../../util/path'; +import type { ClerkBackendApiRequestOptions } from '../request'; import type { MachineToken } from '../resources/MachineToken'; import { AbstractAPI } from './AbstractApi'; const basePath = '/m2m_tokens'; +type WithMachineTokenSecret = T & { machineTokenSecret?: string | null }; + +type CreateMachineTokenParams = WithMachineTokenSecret<{ + name: string; + subject: string; + claims?: Record | null; + scopes?: string[]; + createdBy?: string | null; + secondsUntilExpiration?: number | null; +}>; + +type UpdateMachineTokenParams = WithMachineTokenSecret< + { + m2mTokenId: string; + revoked?: boolean; + } & Pick +>; + +type RevokeMachineTokenParams = WithMachineTokenSecret<{ + m2mTokenId: string; + revocationReason?: string | null; +}>; + export class MachineTokensApi extends AbstractAPI { + /** + * Attaches the machine token secret as an Authorization header if present. + */ + #withMachineTokenSecretHeader>( + options: ClerkBackendApiRequestOptions, + params: T, + ): ClerkBackendApiRequestOptions { + if (params.machineTokenSecret) { + return { + ...options, + headerParams: { + Authorization: `Bearer ${params.machineTokenSecret}`, + }, + }; + } + return options; + } + + async create(params: CreateMachineTokenParams) { + return this.request( + this.#withMachineTokenSecretHeader( + { + method: 'POST', + path: basePath, + bodyParams: params, + }, + params, + ), + ); + } + + async update(params: UpdateMachineTokenParams) { + const { m2mTokenId, ...bodyParams } = params; + this.requireId(m2mTokenId); + return this.request( + this.#withMachineTokenSecretHeader( + { + method: 'PATCH', + path: joinPaths(basePath, m2mTokenId), + bodyParams, + }, + params, + ), + ); + } + + async revoke(params: RevokeMachineTokenParams) { + const { m2mTokenId, ...bodyParams } = params; + this.requireId(m2mTokenId); + return this.request( + this.#withMachineTokenSecretHeader( + { + method: 'POST', + path: joinPaths(basePath, m2mTokenId, 'revoke'), + bodyParams, + }, + params, + ), + ); + } + async verifySecret(secret: string) { return this.request({ method: 'POST', diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index 59eacf4fd0d..8ab4a80df9a 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -108,7 +108,7 @@ export function buildRequest(options: BuildRequestOptions) { ...headerParams, }; - if (secretKey) { + if (secretKey && !headers.Authorization) { headers.Authorization = `Bearer ${secretKey}`; } From ca7a8be3f4c8af1561dd87c29923d44db8a7d65b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 30 Jun 2025 21:36:21 -0700 Subject: [PATCH 02/51] chore: clean up --- .../src/api/endpoints/MachineTokensApi.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 75887a5841b..0484bd43dd3 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -29,18 +29,15 @@ type RevokeMachineTokenParams = WithMachineTokenSecret<{ }>; export class MachineTokensApi extends AbstractAPI { - /** - * Attaches the machine token secret as an Authorization header if present. - */ - #withMachineTokenSecretHeader>( + #withMachineTokenSecretHeader( options: ClerkBackendApiRequestOptions, - params: T, + machineTokenSecret?: string | null, ): ClerkBackendApiRequestOptions { - if (params.machineTokenSecret) { + if (machineTokenSecret) { return { ...options, headerParams: { - Authorization: `Bearer ${params.machineTokenSecret}`, + Authorization: `Bearer ${machineTokenSecret}`, }, }; } @@ -48,20 +45,21 @@ export class MachineTokensApi extends AbstractAPI { } async create(params: CreateMachineTokenParams) { + const { machineTokenSecret, ...bodyParams } = params; return this.request( this.#withMachineTokenSecretHeader( { method: 'POST', path: basePath, - bodyParams: params, + bodyParams, }, - params, + machineTokenSecret, ), ); } async update(params: UpdateMachineTokenParams) { - const { m2mTokenId, ...bodyParams } = params; + const { m2mTokenId, machineTokenSecret, ...bodyParams } = params; this.requireId(m2mTokenId); return this.request( this.#withMachineTokenSecretHeader( @@ -70,13 +68,13 @@ export class MachineTokensApi extends AbstractAPI { path: joinPaths(basePath, m2mTokenId), bodyParams, }, - params, + machineTokenSecret, ), ); } async revoke(params: RevokeMachineTokenParams) { - const { m2mTokenId, ...bodyParams } = params; + const { m2mTokenId, machineTokenSecret, ...bodyParams } = params; this.requireId(m2mTokenId); return this.request( this.#withMachineTokenSecretHeader( @@ -85,7 +83,7 @@ export class MachineTokensApi extends AbstractAPI { path: joinPaths(basePath, m2mTokenId, 'revoke'), bodyParams, }, - params, + machineTokenSecret, ), ); } From af6a27b1aaae8491b7148fb503f1f94f2d03845b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 1 Jul 2025 07:27:25 -0700 Subject: [PATCH 03/51] chore: use a more readable option for bapi proxy methods --- packages/backend/src/api/factory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index ce83dac4328..5283aafbc09 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -68,6 +68,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { buildRequest({ ...options, skipApiVersionInUrl: true, + requireSecretKey: false, }), ), oauthApplications: new OAuthApplicationsApi(request), From fa942278dbdc71cc1b40ddee67e30a9b4e5cb408 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Tue, 1 Jul 2025 08:13:09 -0700 Subject: [PATCH 04/51] chore: add initial changeset --- .changeset/hot-tables-worry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hot-tables-worry.md diff --git a/.changeset/hot-tables-worry.md b/.changeset/hot-tables-worry.md new file mode 100644 index 00000000000..2637253987a --- /dev/null +++ b/.changeset/hot-tables-worry.md @@ -0,0 +1,5 @@ +--- +"@clerk/backend": minor +--- + +WIP M2M Tokens From 8dcd6076c7850c5e6955f174c83ac88e277ba825 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 1 Jul 2025 11:43:40 -0700 Subject: [PATCH 05/51] chore: add machine_secret_key type to api keys api --- packages/backend/src/api/endpoints/APIKeysApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/api/endpoints/APIKeysApi.ts b/packages/backend/src/api/endpoints/APIKeysApi.ts index bf0767d3a16..7e7d706a7a7 100644 --- a/packages/backend/src/api/endpoints/APIKeysApi.ts +++ b/packages/backend/src/api/endpoints/APIKeysApi.ts @@ -5,7 +5,7 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/api_keys'; type CreateAPIKeyParams = { - type?: 'api_key'; + type?: 'api_key' | 'machine_secret_key'; /** * API key name */ From 7bb3eb81f28a4913e5410f98cf353d7d0d282d9e Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 1 Jul 2025 14:31:07 -0700 Subject: [PATCH 06/51] chore: reuse header consts --- packages/backend/src/api/request.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index a5c89e19212..29c5d1b3dcb 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -104,12 +104,12 @@ export function buildRequest(options: BuildRequestOptions) { // Build headers const headers = new Headers({ 'Clerk-API-Version': SUPPORTED_BAPI_VERSION, - 'User-Agent': userAgent, + [constants.Headers.UserAgent]: userAgent, ...headerParams, }); - if (secretKey && !headers.has('Authorization')) { - headers.set('Authorization', `Bearer ${secretKey}`); + if (secretKey && !headers.has(constants.Headers.Authorization)) { + headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`); } let res: Response | undefined; From 424a5a468c8f5e77945177fd7b33723af8021d52 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 1 Jul 2025 14:55:08 -0700 Subject: [PATCH 07/51] chore: rename to machine secret --- .../src/api/endpoints/MachineTokensApi.ts | 48 +++++++++++-------- packages/backend/src/tokens/verify.ts | 2 +- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 0484bd43dd3..11f038ef638 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -5,9 +5,9 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/m2m_tokens'; -type WithMachineTokenSecret = T & { machineTokenSecret?: string | null }; +type WithMachineSecret = T & { machineSecret?: string | null }; -type CreateMachineTokenParams = WithMachineTokenSecret<{ +type CreateMachineTokenParams = WithMachineSecret<{ name: string; subject: string; claims?: Record | null; @@ -16,28 +16,32 @@ type CreateMachineTokenParams = WithMachineTokenSecret<{ secondsUntilExpiration?: number | null; }>; -type UpdateMachineTokenParams = WithMachineTokenSecret< +type UpdateMachineTokenParams = WithMachineSecret< { m2mTokenId: string; revoked?: boolean; } & Pick >; -type RevokeMachineTokenParams = WithMachineTokenSecret<{ +type RevokeMachineTokenParams = WithMachineSecret<{ m2mTokenId: string; revocationReason?: string | null; }>; +type VerifyMachineTokenParams = WithMachineSecret<{ + secret: string; +}>; + export class MachineTokensApi extends AbstractAPI { #withMachineTokenSecretHeader( options: ClerkBackendApiRequestOptions, - machineTokenSecret?: string | null, + machineSecret?: string | null, ): ClerkBackendApiRequestOptions { - if (machineTokenSecret) { + if (machineSecret) { return { ...options, headerParams: { - Authorization: `Bearer ${machineTokenSecret}`, + Authorization: `Bearer ${machineSecret}`, }, }; } @@ -45,7 +49,7 @@ export class MachineTokensApi extends AbstractAPI { } async create(params: CreateMachineTokenParams) { - const { machineTokenSecret, ...bodyParams } = params; + const { machineSecret, ...bodyParams } = params; return this.request( this.#withMachineTokenSecretHeader( { @@ -53,13 +57,13 @@ export class MachineTokensApi extends AbstractAPI { path: basePath, bodyParams, }, - machineTokenSecret, + machineSecret, ), ); } async update(params: UpdateMachineTokenParams) { - const { m2mTokenId, machineTokenSecret, ...bodyParams } = params; + const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); return this.request( this.#withMachineTokenSecretHeader( @@ -68,13 +72,13 @@ export class MachineTokensApi extends AbstractAPI { path: joinPaths(basePath, m2mTokenId), bodyParams, }, - machineTokenSecret, + machineSecret, ), ); } async revoke(params: RevokeMachineTokenParams) { - const { m2mTokenId, machineTokenSecret, ...bodyParams } = params; + const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); return this.request( this.#withMachineTokenSecretHeader( @@ -83,16 +87,22 @@ export class MachineTokensApi extends AbstractAPI { path: joinPaths(basePath, m2mTokenId, 'revoke'), bodyParams, }, - machineTokenSecret, + machineSecret, ), ); } - async verifySecret(secret: string) { - return this.request({ - method: 'POST', - path: joinPaths(basePath, 'verify'), - bodyParams: { secret }, - }); + async verifySecret(params: VerifyMachineTokenParams) { + const { secret, machineSecret } = params; + return this.request( + this.#withMachineTokenSecretHeader( + { + method: 'POST', + path: joinPaths(basePath, 'verify'), + bodyParams: { secret }, + }, + machineSecret, + ), + ); } } diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index ad76138290b..79cc31e9176 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -206,7 +206,7 @@ async function verifyMachineToken( ): Promise> { try { const client = createBackendApiClient(options); - const verifiedToken = await client.machineTokens.verifySecret(secret); + const verifiedToken = await client.machineTokens.verifySecret({ secret }); return { data: verifiedToken, tokenType: TokenType.MachineToken, errors: undefined }; } catch (err: any) { return handleClerkAPIError(TokenType.MachineToken, err, 'Machine token not found'); From 1dbd41b5b92f46d59a56a82c1b5c1172eb5bd13d Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 1 Jul 2025 15:00:40 -0700 Subject: [PATCH 08/51] chore: clean up --- .../backend/src/api/endpoints/MachineTokensApi.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 11f038ef638..eb5a70d3b62 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -33,7 +33,10 @@ type VerifyMachineTokenParams = WithMachineSecret<{ }>; export class MachineTokensApi extends AbstractAPI { - #withMachineTokenSecretHeader( + /** + * Overrides the instance secret with the machine secret. + */ + #withMachineSecretHeader( options: ClerkBackendApiRequestOptions, machineSecret?: string | null, ): ClerkBackendApiRequestOptions { @@ -51,7 +54,7 @@ export class MachineTokensApi extends AbstractAPI { async create(params: CreateMachineTokenParams) { const { machineSecret, ...bodyParams } = params; return this.request( - this.#withMachineTokenSecretHeader( + this.#withMachineSecretHeader( { method: 'POST', path: basePath, @@ -66,7 +69,7 @@ export class MachineTokensApi extends AbstractAPI { const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); return this.request( - this.#withMachineTokenSecretHeader( + this.#withMachineSecretHeader( { method: 'PATCH', path: joinPaths(basePath, m2mTokenId), @@ -81,7 +84,7 @@ export class MachineTokensApi extends AbstractAPI { const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); return this.request( - this.#withMachineTokenSecretHeader( + this.#withMachineSecretHeader( { method: 'POST', path: joinPaths(basePath, m2mTokenId, 'revoke'), @@ -95,7 +98,7 @@ export class MachineTokensApi extends AbstractAPI { async verifySecret(params: VerifyMachineTokenParams) { const { secret, machineSecret } = params; return this.request( - this.#withMachineTokenSecretHeader( + this.#withMachineSecretHeader( { method: 'POST', path: joinPaths(basePath, 'verify'), From 7c3063c4cbceeb699dd3937f03dd50c2e3104b6a Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 1 Jul 2025 15:55:30 -0700 Subject: [PATCH 09/51] chore: add secret property to create method --- packages/backend/src/api/endpoints/MachineTokensApi.ts | 8 +++++--- packages/backend/src/api/resources/JSON.ts | 1 + packages/backend/src/api/resources/MachineToken.ts | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index eb5a70d3b62..aa4fb2bbc33 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -32,6 +32,8 @@ type VerifyMachineTokenParams = WithMachineSecret<{ secret: string; }>; +type MachineTokenWithoutSecret = Omit; + export class MachineTokensApi extends AbstractAPI { /** * Overrides the instance secret with the machine secret. @@ -68,7 +70,7 @@ export class MachineTokensApi extends AbstractAPI { async update(params: UpdateMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); - return this.request( + return this.request( this.#withMachineSecretHeader( { method: 'PATCH', @@ -83,7 +85,7 @@ export class MachineTokensApi extends AbstractAPI { async revoke(params: RevokeMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); - return this.request( + return this.request( this.#withMachineSecretHeader( { method: 'POST', @@ -97,7 +99,7 @@ export class MachineTokensApi extends AbstractAPI { async verifySecret(params: VerifyMachineTokenParams) { const { secret, machineSecret } = params; - return this.request( + return this.request( this.#withMachineSecretHeader( { method: 'POST', diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index faea4ed7424..cb697a720ba 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -701,6 +701,7 @@ export interface SamlAccountConnectionJSON extends ClerkResourceJSON { export interface MachineTokenJSON extends ClerkResourceJSON { object: typeof ObjectType.MachineToken; name: string; + secret: string; subject: string; scopes: string[]; claims: Record | null; diff --git a/packages/backend/src/api/resources/MachineToken.ts b/packages/backend/src/api/resources/MachineToken.ts index 1d19837bcdf..40cb3ae65fc 100644 --- a/packages/backend/src/api/resources/MachineToken.ts +++ b/packages/backend/src/api/resources/MachineToken.ts @@ -4,6 +4,7 @@ export class MachineToken { constructor( readonly id: string, readonly name: string, + readonly secret: string, readonly subject: string, readonly scopes: string[], readonly claims: Record | null, @@ -17,10 +18,11 @@ export class MachineToken { readonly updatedAt: number, ) {} - static fromJSON(data: MachineTokenJSON) { + static fromJSON(data: MachineTokenJSON): MachineToken { return new MachineToken( data.id, data.name, + data.secret, data.subject, data.scopes, data.claims, From db38ca5bd8714bd780ebe009ed46d084713345d4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 1 Jul 2025 16:22:29 -0700 Subject: [PATCH 10/51] chore: remove machine secret type from api key creation --- packages/backend/src/api/endpoints/APIKeysApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/api/endpoints/APIKeysApi.ts b/packages/backend/src/api/endpoints/APIKeysApi.ts index 7e7d706a7a7..bf0767d3a16 100644 --- a/packages/backend/src/api/endpoints/APIKeysApi.ts +++ b/packages/backend/src/api/endpoints/APIKeysApi.ts @@ -5,7 +5,7 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/api_keys'; type CreateAPIKeyParams = { - type?: 'api_key' | 'machine_secret_key'; + type?: 'api_key'; /** * API key name */ From 5ce88eece43a498006680ec2f3655136740b2702 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 2 Jul 2025 13:47:04 -0700 Subject: [PATCH 11/51] chore: make secret property optional --- packages/backend/src/api/endpoints/MachineTokensApi.ts | 8 +++----- packages/backend/src/api/resources/JSON.ts | 2 +- packages/backend/src/api/resources/MachineToken.ts | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index aa4fb2bbc33..eb5a70d3b62 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -32,8 +32,6 @@ type VerifyMachineTokenParams = WithMachineSecret<{ secret: string; }>; -type MachineTokenWithoutSecret = Omit; - export class MachineTokensApi extends AbstractAPI { /** * Overrides the instance secret with the machine secret. @@ -70,7 +68,7 @@ export class MachineTokensApi extends AbstractAPI { async update(params: UpdateMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); - return this.request( + return this.request( this.#withMachineSecretHeader( { method: 'PATCH', @@ -85,7 +83,7 @@ export class MachineTokensApi extends AbstractAPI { async revoke(params: RevokeMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); - return this.request( + return this.request( this.#withMachineSecretHeader( { method: 'POST', @@ -99,7 +97,7 @@ export class MachineTokensApi extends AbstractAPI { async verifySecret(params: VerifyMachineTokenParams) { const { secret, machineSecret } = params; - return this.request( + return this.request( this.#withMachineSecretHeader( { method: 'POST', diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index cb697a720ba..3d000941ef3 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -701,7 +701,7 @@ export interface SamlAccountConnectionJSON extends ClerkResourceJSON { export interface MachineTokenJSON extends ClerkResourceJSON { object: typeof ObjectType.MachineToken; name: string; - secret: string; + secret?: string; subject: string; scopes: string[]; claims: Record | null; diff --git a/packages/backend/src/api/resources/MachineToken.ts b/packages/backend/src/api/resources/MachineToken.ts index 40cb3ae65fc..3b9340c09dc 100644 --- a/packages/backend/src/api/resources/MachineToken.ts +++ b/packages/backend/src/api/resources/MachineToken.ts @@ -4,7 +4,6 @@ export class MachineToken { constructor( readonly id: string, readonly name: string, - readonly secret: string, readonly subject: string, readonly scopes: string[], readonly claims: Record | null, @@ -16,13 +15,13 @@ export class MachineToken { readonly creationReason: string | null, readonly createdAt: number, readonly updatedAt: number, + readonly secret?: string, ) {} static fromJSON(data: MachineTokenJSON): MachineToken { return new MachineToken( data.id, data.name, - data.secret, data.subject, data.scopes, data.claims, @@ -34,6 +33,7 @@ export class MachineToken { data.creation_reason, data.created_at, data.updated_at, + data.secret, ); } } From e900e136b020b79e1e2d9e33150569b233b09360 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 8 Jul 2025 13:32:39 -0700 Subject: [PATCH 12/51] chore: add machines BAPI endpoints --- .../backend/src/api/endpoints/MachineApi.ts | 70 +++++++++++++++++++ packages/backend/src/api/endpoints/index.ts | 1 + packages/backend/src/api/factory.ts | 2 + .../backend/src/api/resources/Deserializer.ts | 3 + packages/backend/src/api/resources/JSON.ts | 10 +++ packages/backend/src/api/resources/Machine.ts | 15 ++++ packages/backend/src/api/resources/index.ts | 1 + 7 files changed, 102 insertions(+) create mode 100644 packages/backend/src/api/endpoints/MachineApi.ts create mode 100644 packages/backend/src/api/resources/Machine.ts diff --git a/packages/backend/src/api/endpoints/MachineApi.ts b/packages/backend/src/api/endpoints/MachineApi.ts new file mode 100644 index 00000000000..aee3029de75 --- /dev/null +++ b/packages/backend/src/api/endpoints/MachineApi.ts @@ -0,0 +1,70 @@ +import { joinPaths } from '../../util/path'; +import type { PaginatedResourceResponse } from '../resources/Deserializer'; +import type { Machine } from '../resources/Machine'; +import { AbstractAPI } from './AbstractApi'; + +const basePath = '/machines'; + +type CreateMachineParams = { + name: string; +}; + +type UpdateMachineParams = { + machineId: string; + name: string; +}; + +type DeleteMachineParams = { + machineId: string; +}; + +type GetMachineListParams = { + limit?: number; + offset?: number; + query?: string; +}; + +export class MachineApi extends AbstractAPI { + async list(queryParams: GetMachineListParams = {}) { + return this.request>({ + method: 'GET', + path: basePath, + queryParams, + }); + } + + async create(bodyParams: CreateMachineParams) { + return this.request({ + method: 'POST', + path: basePath, + bodyParams, + }); + } + + async update(params: UpdateMachineParams) { + const { machineId, ...bodyParams } = params; + this.requireId(machineId); + return this.request({ + method: 'PATCH', + path: joinPaths(basePath, machineId), + bodyParams, + }); + } + + async delete(params: DeleteMachineParams) { + const { machineId } = params; + this.requireId(machineId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, machineId), + }); + } + + async get(machineId: string) { + this.requireId(machineId); + return this.request({ + method: 'GET', + path: joinPaths(basePath, machineId), + }); + } +} diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index 26b3f2e8d3f..e7eeb312c68 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -11,6 +11,7 @@ export * from './EmailAddressApi'; export * from './IdPOAuthAccessTokenApi'; export * from './InstanceApi'; export * from './InvitationApi'; +export * from './MachineApi'; export * from './MachineTokensApi'; export * from './JwksApi'; export * from './JwtTemplatesApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index 5283aafbc09..b2817cc10ba 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -13,6 +13,7 @@ import { InvitationAPI, JwksAPI, JwtTemplatesApi, + MachineApi, MachineTokensApi, OAuthApplicationsApi, OrganizationAPI, @@ -64,6 +65,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { invitations: new InvitationAPI(request), jwks: new JwksAPI(request), jwtTemplates: new JwtTemplatesApi(request), + machines: new MachineApi(request), machineTokens: new MachineTokensApi( buildRequest({ ...options, diff --git a/packages/backend/src/api/resources/Deserializer.ts b/packages/backend/src/api/resources/Deserializer.ts index 4f47da13e50..2db6e993609 100644 --- a/packages/backend/src/api/resources/Deserializer.ts +++ b/packages/backend/src/api/resources/Deserializer.ts @@ -15,6 +15,7 @@ import { InstanceSettings, Invitation, JwtTemplate, + Machine, MachineToken, OauthAccessToken, OAuthApplication, @@ -132,6 +133,8 @@ function jsonToObject(item: any): any { return Invitation.fromJSON(item); case ObjectType.JwtTemplate: return JwtTemplate.fromJSON(item); + case ObjectType.Machine: + return Machine.fromJSON(item); case ObjectType.MachineToken: return MachineToken.fromJSON(item); case ObjectType.OauthAccessToken: diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index e7f7aaec564..682a4ffee42 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -34,6 +34,7 @@ export const ObjectType = { InstanceRestrictions: 'instance_restrictions', InstanceSettings: 'instance_settings', Invitation: 'invitation', + Machine: 'machine', MachineToken: 'machine_to_machine_token', JwtTemplate: 'jwt_template', OauthAccessToken: 'oauth_access_token', @@ -698,6 +699,15 @@ export interface SamlAccountConnectionJSON extends ClerkResourceJSON { updated_at: number; } +export interface MachineJSON extends ClerkResourceJSON { + object: typeof ObjectType.Machine; + id: string; + name: string; + instance_id: string; + created_at: number; + updated_at: number; +} + export interface MachineTokenJSON extends ClerkResourceJSON { object: typeof ObjectType.MachineToken; name: string; diff --git a/packages/backend/src/api/resources/Machine.ts b/packages/backend/src/api/resources/Machine.ts new file mode 100644 index 00000000000..16b2f9b010f --- /dev/null +++ b/packages/backend/src/api/resources/Machine.ts @@ -0,0 +1,15 @@ +import type { MachineJSON } from './JSON'; + +export class Machine { + constructor( + readonly id: string, + readonly name: string, + readonly instanceId: string, + readonly createdAt: number, + readonly updatedAt: number, + ) {} + + static fromJSON(data: MachineJSON): Machine { + return new Machine(data.id, data.name, data.instance_id, data.created_at, data.updated_at); + } +} diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 1353e249ab7..034ab10fd3e 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -30,6 +30,7 @@ export * from './InstanceRestrictions'; export * from './InstanceSettings'; export * from './Invitation'; export * from './JSON'; +export * from './Machine'; export * from './MachineToken'; export * from './JwtTemplate'; export * from './OauthAccessToken'; From 6c0fc641823b1fcbfdfa20c796c3dc72d134e657 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 8 Jul 2025 13:38:06 -0700 Subject: [PATCH 13/51] chore: trigger rebuild --- packages/backend/src/api/endpoints/MachineTokensApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index eb5a70d3b62..9b6faa28ab8 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -34,7 +34,7 @@ type VerifyMachineTokenParams = WithMachineSecret<{ export class MachineTokensApi extends AbstractAPI { /** - * Overrides the instance secret with the machine secret. + * Overrides the instance secret with a machine secret. */ #withMachineSecretHeader( options: ClerkBackendApiRequestOptions, From c1d1ae2fc09d3d35dbd116ad2b1e90a18aca7af8 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 8 Jul 2025 13:48:32 -0700 Subject: [PATCH 14/51] chore: remove unnecessary params --- packages/backend/src/api/endpoints/MachineApi.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineApi.ts b/packages/backend/src/api/endpoints/MachineApi.ts index aee3029de75..fafdc4868f7 100644 --- a/packages/backend/src/api/endpoints/MachineApi.ts +++ b/packages/backend/src/api/endpoints/MachineApi.ts @@ -14,10 +14,6 @@ type UpdateMachineParams = { name: string; }; -type DeleteMachineParams = { - machineId: string; -}; - type GetMachineListParams = { limit?: number; offset?: number; @@ -51,8 +47,7 @@ export class MachineApi extends AbstractAPI { }); } - async delete(params: DeleteMachineParams) { - const { machineId } = params; + async delete(machineId: string) { this.requireId(machineId); return this.request({ method: 'DELETE', From 7ff05382f2cdf3868b3ffbc77a2c1e52cd9c0675 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 25 Jul 2025 13:12:50 -0700 Subject: [PATCH 15/51] chore: remove unused properties --- packages/backend/src/api/endpoints/MachineTokensApi.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 9b6faa28ab8..817b78b6eff 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -8,19 +8,16 @@ const basePath = '/m2m_tokens'; type WithMachineSecret = T & { machineSecret?: string | null }; type CreateMachineTokenParams = WithMachineSecret<{ - name: string; - subject: string; claims?: Record | null; - scopes?: string[]; - createdBy?: string | null; secondsUntilExpiration?: number | null; }>; type UpdateMachineTokenParams = WithMachineSecret< { m2mTokenId: string; + revocationReason?: string | null; revoked?: boolean; - } & Pick + } & Pick >; type RevokeMachineTokenParams = WithMachineSecret<{ From e8445653816f7030c25ccaf8418da026846c29e2 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 25 Jul 2025 14:17:43 -0700 Subject: [PATCH 16/51] chore: improve machine secret check --- .../src/api/endpoints/MachineTokensApi.ts | 104 +++++++++--------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 817b78b6eff..6496b721c0c 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -30,79 +30,73 @@ type VerifyMachineTokenParams = WithMachineSecret<{ }>; export class MachineTokensApi extends AbstractAPI { - /** - * Overrides the instance secret with a machine secret. - */ - #withMachineSecretHeader( - options: ClerkBackendApiRequestOptions, - machineSecret?: string | null, - ): ClerkBackendApiRequestOptions { - if (machineSecret) { - return { - ...options, - headerParams: { - Authorization: `Bearer ${machineSecret}`, - }, - }; + #requireMachineSecret(machineSecret?: string | null): asserts machineSecret is string { + if (!machineSecret) { + throw new Error('A machine secret is required.'); } - return options; } async create(params: CreateMachineTokenParams) { const { machineSecret, ...bodyParams } = params; - return this.request( - this.#withMachineSecretHeader( - { - method: 'POST', - path: basePath, - bodyParams, - }, - machineSecret, - ), - ); + this.#requireMachineSecret(machineSecret); + return this.request({ + method: 'POST', + path: basePath, + bodyParams, + headerParams: { + Authorization: `Bearer ${machineSecret}`, + }, + }); } async update(params: UpdateMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; + this.#requireMachineSecret(machineSecret); this.requireId(m2mTokenId); - return this.request( - this.#withMachineSecretHeader( - { - method: 'PATCH', - path: joinPaths(basePath, m2mTokenId), - bodyParams, - }, - machineSecret, - ), - ); + return this.request({ + method: 'PATCH', + path: joinPaths(basePath, m2mTokenId), + bodyParams, + headerParams: { + Authorization: `Bearer ${machineSecret}`, + }, + }); } async revoke(params: RevokeMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; this.requireId(m2mTokenId); - return this.request( - this.#withMachineSecretHeader( - { - method: 'POST', - path: joinPaths(basePath, m2mTokenId, 'revoke'), - bodyParams, - }, - machineSecret, - ), - ); + + const requestOptions: ClerkBackendApiRequestOptions = { + method: 'POST', + path: joinPaths(basePath, m2mTokenId, 'revoke'), + bodyParams, + }; + + if (machineSecret) { + requestOptions.headerParams = { + Authorization: `Bearer ${machineSecret}`, + }; + } + + return this.request(requestOptions); } async verifySecret(params: VerifyMachineTokenParams) { const { secret, machineSecret } = params; - return this.request( - this.#withMachineSecretHeader( - { - method: 'POST', - path: joinPaths(basePath, 'verify'), - bodyParams: { secret }, - }, - machineSecret, - ), - ); + + const requestOptions: ClerkBackendApiRequestOptions = { + method: 'POST', + path: joinPaths(basePath, 'verify'), + bodyParams: { secret }, + }; + + if (machineSecret) { + requestOptions.headerParams = { + Authorization: `Bearer ${machineSecret}`, + }; + } + + return this.request(requestOptions); } } From 017bb4baabc127e3b279a18553067ed871b5a171 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 25 Jul 2025 14:30:37 -0700 Subject: [PATCH 17/51] fix required secrets --- .../src/api/endpoints/MachineTokensApi.ts | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 6496b721c0c..d96b18f6145 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -5,29 +5,29 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/m2m_tokens'; -type WithMachineSecret = T & { machineSecret?: string | null }; - -type CreateMachineTokenParams = WithMachineSecret<{ +type CreateMachineTokenParams = { + machineSecret: string; claims?: Record | null; secondsUntilExpiration?: number | null; -}>; +}; -type UpdateMachineTokenParams = WithMachineSecret< - { - m2mTokenId: string; - revocationReason?: string | null; - revoked?: boolean; - } & Pick ->; +type UpdateMachineTokenParams = { + machineSecret: string; + m2mTokenId: string; + revocationReason?: string | null; + revoked?: boolean; +} & Pick; -type RevokeMachineTokenParams = WithMachineSecret<{ +type RevokeMachineTokenParams = { + machineSecret?: string | null; m2mTokenId: string; revocationReason?: string | null; -}>; +}; -type VerifyMachineTokenParams = WithMachineSecret<{ +type VerifyMachineTokenParams = { + machineSecret?: string | null; secret: string; -}>; +}; export class MachineTokensApi extends AbstractAPI { #requireMachineSecret(machineSecret?: string | null): asserts machineSecret is string { @@ -38,7 +38,9 @@ export class MachineTokensApi extends AbstractAPI { async create(params: CreateMachineTokenParams) { const { machineSecret, ...bodyParams } = params; + this.#requireMachineSecret(machineSecret); + return this.request({ method: 'POST', path: basePath, @@ -51,8 +53,10 @@ export class MachineTokensApi extends AbstractAPI { async update(params: UpdateMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; + this.#requireMachineSecret(machineSecret); this.requireId(m2mTokenId); + return this.request({ method: 'PATCH', path: joinPaths(basePath, m2mTokenId), @@ -65,6 +69,7 @@ export class MachineTokensApi extends AbstractAPI { async revoke(params: RevokeMachineTokenParams) { const { m2mTokenId, machineSecret, ...bodyParams } = params; + this.requireId(m2mTokenId); const requestOptions: ClerkBackendApiRequestOptions = { @@ -82,21 +87,13 @@ export class MachineTokensApi extends AbstractAPI { return this.request(requestOptions); } - async verifySecret(params: VerifyMachineTokenParams) { - const { secret, machineSecret } = params; - + async verifySecret(machineSecret: VerifyMachineTokenParams) { const requestOptions: ClerkBackendApiRequestOptions = { method: 'POST', path: joinPaths(basePath, 'verify'), - bodyParams: { secret }, + bodyParams: { secret: machineSecret }, }; - if (machineSecret) { - requestOptions.headerParams = { - Authorization: `Bearer ${machineSecret}`, - }; - } - return this.request(requestOptions); } } From e26660e90326b51f69b63bde03768d08fd60155a Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 25 Jul 2025 14:36:22 -0700 Subject: [PATCH 18/51] fix required secrets --- packages/backend/src/api/endpoints/MachineTokensApi.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index d96b18f6145..6acc2ae5603 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -24,11 +24,6 @@ type RevokeMachineTokenParams = { revocationReason?: string | null; }; -type VerifyMachineTokenParams = { - machineSecret?: string | null; - secret: string; -}; - export class MachineTokensApi extends AbstractAPI { #requireMachineSecret(machineSecret?: string | null): asserts machineSecret is string { if (!machineSecret) { @@ -87,7 +82,7 @@ export class MachineTokensApi extends AbstractAPI { return this.request(requestOptions); } - async verifySecret(machineSecret: VerifyMachineTokenParams) { + async verifySecret(machineSecret: string) { const requestOptions: ClerkBackendApiRequestOptions = { method: 'POST', path: joinPaths(basePath, 'verify'), From 0f7387df9ea81a7cfb83a00eb51bc06aa2397b7e Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 25 Jul 2025 15:49:41 -0700 Subject: [PATCH 19/51] fix required secrets --- .../src/api/endpoints/MachineTokensApi.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 6acc2ae5603..92fed5fe96d 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -24,6 +24,11 @@ type RevokeMachineTokenParams = { revocationReason?: string | null; }; +type VerifyMachineTokenParams = { + machineSecret?: string | null; + secret: string; +}; + export class MachineTokensApi extends AbstractAPI { #requireMachineSecret(machineSecret?: string | null): asserts machineSecret is string { if (!machineSecret) { @@ -82,13 +87,21 @@ export class MachineTokensApi extends AbstractAPI { return this.request(requestOptions); } - async verifySecret(machineSecret: string) { + async verifySecret(params: VerifyMachineTokenParams) { + const { machineSecret, secret } = params; + const requestOptions: ClerkBackendApiRequestOptions = { method: 'POST', path: joinPaths(basePath, 'verify'), - bodyParams: { secret: machineSecret }, + bodyParams: { secret }, }; + if (machineSecret) { + requestOptions.headerParams = { + Authorization: `Bearer ${machineSecret}`, + }; + } + return this.request(requestOptions); } } From f78ddcc3a3a5cd01d4867ebd2e0414175352a667 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 25 Jul 2025 15:53:44 -0700 Subject: [PATCH 20/51] chore: remove removed properties --- packages/backend/src/api/resources/JSON.ts | 3 --- packages/backend/src/api/resources/MachineToken.ts | 6 ------ 2 files changed, 9 deletions(-) diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index f67c8745886..fd92d92e92b 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -714,7 +714,6 @@ export interface MachineJSON extends ClerkResourceJSON { export interface MachineTokenJSON extends ClerkResourceJSON { object: typeof ObjectType.MachineToken; - name: string; secret?: string; subject: string; scopes: string[]; @@ -723,8 +722,6 @@ export interface MachineTokenJSON extends ClerkResourceJSON { revocation_reason: string | null; expired: boolean; expiration: number | null; - created_by: string | null; - creation_reason: string | null; created_at: number; updated_at: number; } diff --git a/packages/backend/src/api/resources/MachineToken.ts b/packages/backend/src/api/resources/MachineToken.ts index 3b9340c09dc..2ee4b69a69d 100644 --- a/packages/backend/src/api/resources/MachineToken.ts +++ b/packages/backend/src/api/resources/MachineToken.ts @@ -3,7 +3,6 @@ import type { MachineTokenJSON } from './JSON'; export class MachineToken { constructor( readonly id: string, - readonly name: string, readonly subject: string, readonly scopes: string[], readonly claims: Record | null, @@ -11,8 +10,6 @@ export class MachineToken { readonly revocationReason: string | null, readonly expired: boolean, readonly expiration: number | null, - readonly createdBy: string | null, - readonly creationReason: string | null, readonly createdAt: number, readonly updatedAt: number, readonly secret?: string, @@ -21,7 +18,6 @@ export class MachineToken { static fromJSON(data: MachineTokenJSON): MachineToken { return new MachineToken( data.id, - data.name, data.subject, data.scopes, data.claims, @@ -29,8 +25,6 @@ export class MachineToken { data.revocation_reason, data.expired, data.expiration, - data.created_by, - data.creation_reason, data.created_at, data.updated_at, data.secret, From a8c66e1ef3c079e2e9febc18b13f8335913e6768 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 28 Jul 2025 16:14:20 -0700 Subject: [PATCH 21/51] chore: remove name and claims from m2m tokens --- .../src/api/__tests__/MachineTokenApi.ts | 90 +++++++++++++++++++ ...MachineTokensApi.ts => MachineTokenApi.ts} | 33 ++----- packages/backend/src/api/endpoints/index.ts | 2 +- packages/backend/src/api/factory.ts | 4 +- packages/backend/src/api/request.ts | 13 ++- packages/backend/src/api/resources/JSON.ts | 1 - .../backend/src/api/resources/MachineToken.ts | 2 - .../src/tokens/__tests__/authObjects.test.ts | 3 - .../src/tokens/__tests__/authStatus.test.ts | 3 +- .../src/tokens/__tests__/verify.test.ts | 2 - packages/backend/src/tokens/authObjects.ts | 4 - 11 files changed, 113 insertions(+), 44 deletions(-) create mode 100644 packages/backend/src/api/__tests__/MachineTokenApi.ts rename packages/backend/src/api/endpoints/{MachineTokensApi.ts => MachineTokenApi.ts} (74%) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.ts b/packages/backend/src/api/__tests__/MachineTokenApi.ts new file mode 100644 index 00000000000..d6733cf871d --- /dev/null +++ b/packages/backend/src/api/__tests__/MachineTokenApi.ts @@ -0,0 +1,90 @@ +import { http, HttpResponse } from 'msw'; +import { describe, expect, it } from 'vitest'; + +import { server, validateHeaders } from '../../mock-server'; +import { createBackendApiClient } from '../factory'; + +describe('MachineTokenAPI', () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + const m2mId = 'mt_xxxxx'; + const m2mSecret = 'mt_secret_xxxxx'; + + const mockM2MToken = { + object: 'machine_to_machine_token', + id: m2mId, + subject: 'mch_xxxxx', + scopes: [], + secret: m2mSecret, + revoked: false, + revocationReason: null, + expired: false, + expiration: 1753746916590, + createdAt: 1753743316590, + updatedAt: 1753743316590, + }; + + it('creates a machine-to-machine token', async () => { + const createParams = { + machineSecret: 'ak_xxxxx', + secondsUntilExpiration: 3600, + }; + + server.use( + http.get( + `https://api.clerk.test/m2m_tokens`, + validateHeaders(() => { + return HttpResponse.json(mockM2MToken); + }), + ), + ); + + const response = await apiClient.machineTokens.create(createParams); + + expect(response.id).toBe(m2mId); + expect(response.secret).toBe(m2mSecret); + }); + + it('revokes a machine-to-machine token', async () => { + const revokeParams = { + machineSecret: 'ak_xxxxx', + m2mTokenId: m2mId, + }; + + server.use( + http.post(`https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, () => { + return HttpResponse.json({ + ...mockM2MToken, + revoked: true, + revocationReason: 'revoked by test', + }); + }), + ); + + const response = await apiClient.machineTokens.revoke(revokeParams); + + expect(response.revoked).toBe(true); + expect(response.revocationReason).toBe('revoked by test'); + }); + + it('verifies a machine-to-machine token', async () => { + const verifyParams = { + machineSecret: 'ak_xxxxx', + m2mTokenId: m2mId, + secret: m2mSecret, + }; + + server.use( + http.post('https://api.clerk.test/m2m_tokens/verify', () => { + return HttpResponse.json(mockM2MToken); + }), + ); + + const response = await apiClient.machineTokens.verifySecret(verifyParams); + + expect(response.id).toBe(m2mId); + expect(response.secret).toBe(m2mSecret); + }); +}); diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokenApi.ts similarity index 74% rename from packages/backend/src/api/endpoints/MachineTokensApi.ts rename to packages/backend/src/api/endpoints/MachineTokenApi.ts index 92fed5fe96d..07f7da912f8 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokenApi.ts @@ -7,17 +7,9 @@ const basePath = '/m2m_tokens'; type CreateMachineTokenParams = { machineSecret: string; - claims?: Record | null; secondsUntilExpiration?: number | null; }; -type UpdateMachineTokenParams = { - machineSecret: string; - m2mTokenId: string; - revocationReason?: string | null; - revoked?: boolean; -} & Pick; - type RevokeMachineTokenParams = { machineSecret?: string | null; m2mTokenId: string; @@ -29,7 +21,7 @@ type VerifyMachineTokenParams = { secret: string; }; -export class MachineTokensApi extends AbstractAPI { +export class MachineTokenApi extends AbstractAPI { #requireMachineSecret(machineSecret?: string | null): asserts machineSecret is string { if (!machineSecret) { throw new Error('A machine secret is required.'); @@ -45,22 +37,9 @@ export class MachineTokensApi extends AbstractAPI { method: 'POST', path: basePath, bodyParams, - headerParams: { - Authorization: `Bearer ${machineSecret}`, + options: { + skipSecretKeyAuthorization: true, }, - }); - } - - async update(params: UpdateMachineTokenParams) { - const { m2mTokenId, machineSecret, ...bodyParams } = params; - - this.#requireMachineSecret(machineSecret); - this.requireId(m2mTokenId); - - return this.request({ - method: 'PATCH', - path: joinPaths(basePath, m2mTokenId), - bodyParams, headerParams: { Authorization: `Bearer ${machineSecret}`, }, @@ -79,6 +58,9 @@ export class MachineTokensApi extends AbstractAPI { }; if (machineSecret) { + requestOptions.options = { + skipSecretKeyAuthorization: true, + }; requestOptions.headerParams = { Authorization: `Bearer ${machineSecret}`, }; @@ -97,6 +79,9 @@ export class MachineTokensApi extends AbstractAPI { }; if (machineSecret) { + requestOptions.options = { + skipSecretKeyAuthorization: true, + }; requestOptions.headerParams = { Authorization: `Bearer ${machineSecret}`, }; diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index e7eeb312c68..df9984cf2b3 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -12,7 +12,7 @@ export * from './IdPOAuthAccessTokenApi'; export * from './InstanceApi'; export * from './InvitationApi'; export * from './MachineApi'; -export * from './MachineTokensApi'; +export * from './MachineTokenApi'; export * from './JwksApi'; export * from './JwtTemplatesApi'; export * from './OrganizationApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index b2817cc10ba..6af1dba5581 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -14,7 +14,7 @@ import { JwksAPI, JwtTemplatesApi, MachineApi, - MachineTokensApi, + MachineTokenApi, OAuthApplicationsApi, OrganizationAPI, PhoneNumberAPI, @@ -66,7 +66,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { jwks: new JwksAPI(request), jwtTemplates: new JwtTemplatesApi(request), machines: new MachineApi(request), - machineTokens: new MachineTokensApi( + machineTokens: new MachineTokenApi( buildRequest({ ...options, skipApiVersionInUrl: true, diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index ab981afe77b..bbe8c9a3c97 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -27,12 +27,19 @@ type ClerkBackendApiRequestOptionsBodyParams = * @default false */ deepSnakecaseBodyParamKeys?: boolean; + /** + * If true, skips adding the instance secret key to the authorization header. + * Useful when you need to provide custom authorization (e.g., machine secrets). + * @default false + */ + skipSecretKeyAuthorization?: boolean; }; } | { bodyParams?: never; options?: { - deepSnakecaseBodyParamKeys?: never; + deepSnakecaseBodyParamKeys?: boolean; + skipSecretKeyAuthorization?: never; }; }; @@ -98,7 +105,7 @@ export function buildRequest(options: BuildRequestOptions) { skipApiVersionInUrl = false, } = options; const { path, method, queryParams, headerParams, bodyParams, formData, options: opts } = requestOptions; - const { deepSnakecaseBodyParamKeys = false } = opts || {}; + const { deepSnakecaseBodyParamKeys = false, skipSecretKeyAuthorization = false } = opts || {}; if (requireSecretKey) { assertValidSecretKey(secretKey); @@ -128,7 +135,7 @@ export function buildRequest(options: BuildRequestOptions) { ...headerParams, }); - if (secretKey && !headers.has(constants.Headers.Authorization)) { + if (secretKey && !skipSecretKeyAuthorization) { headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`); } diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 87bc0d1a379..49fa8aacda5 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -734,7 +734,6 @@ export interface MachineTokenJSON extends ClerkResourceJSON { secret?: string; subject: string; scopes: string[]; - claims: Record | null; revoked: boolean; revocation_reason: string | null; expired: boolean; diff --git a/packages/backend/src/api/resources/MachineToken.ts b/packages/backend/src/api/resources/MachineToken.ts index 2ee4b69a69d..d9076b68555 100644 --- a/packages/backend/src/api/resources/MachineToken.ts +++ b/packages/backend/src/api/resources/MachineToken.ts @@ -5,7 +5,6 @@ export class MachineToken { readonly id: string, readonly subject: string, readonly scopes: string[], - readonly claims: Record | null, readonly revoked: boolean, readonly revocationReason: string | null, readonly expired: boolean, @@ -20,7 +19,6 @@ export class MachineToken { data.id, data.subject, data.scopes, - data.claims, data.revoked, data.revocation_reason, data.expired, diff --git a/packages/backend/src/tokens/__tests__/authObjects.test.ts b/packages/backend/src/tokens/__tests__/authObjects.test.ts index 64bfce39818..8006decd716 100644 --- a/packages/backend/src/tokens/__tests__/authObjects.test.ts +++ b/packages/backend/src/tokens/__tests__/authObjects.test.ts @@ -366,7 +366,6 @@ describe('authenticatedMachineObject', () => { expect(authObject.id).toBe('m2m_ey966f1b1xf93586b2debdcadb0b3bd1'); expect(authObject.subject).toBe('mch_2vYVtestTESTtestTESTtestTESTtest'); expect(authObject.scopes).toEqual(['read:foo', 'write:bar']); - expect(authObject.claims).toEqual({ foo: 'bar' }); expect(authObject.machineId).toBe('mch_2vYVtestTESTtestTESTtestTESTtest'); }); }); @@ -377,10 +376,8 @@ describe('unauthenticatedMachineObject', () => { const authObject = unauthenticatedMachineObject('machine_token'); expect(authObject.tokenType).toBe('machine_token'); expect(authObject.id).toBeNull(); - expect(authObject.name).toBeNull(); expect(authObject.subject).toBeNull(); expect(authObject.scopes).toBeNull(); - expect(authObject.claims).toBeNull(); }); it('has() always returns false', () => { diff --git a/packages/backend/src/tokens/__tests__/authStatus.test.ts b/packages/backend/src/tokens/__tests__/authStatus.test.ts index 8f6dc1f9f2f..527d7885d44 100644 --- a/packages/backend/src/tokens/__tests__/authStatus.test.ts +++ b/packages/backend/src/tokens/__tests__/authStatus.test.ts @@ -125,9 +125,8 @@ describe('signed-out', () => { expect(token).toBeNull(); expect(signedOutAuthObject.tokenType).toBe('machine_token'); expect(signedOutAuthObject.id).toBeNull(); - expect(signedOutAuthObject.name).toBeNull(); expect(signedOutAuthObject.subject).toBeNull(); - expect(signedOutAuthObject.claims).toBeNull(); + expect(signedOutAuthObject.scopes).toBeNull(); }); }); }); diff --git a/packages/backend/src/tokens/__tests__/verify.test.ts b/packages/backend/src/tokens/__tests__/verify.test.ts index 3de15ff314c..b0f12ff9eb3 100644 --- a/packages/backend/src/tokens/__tests__/verify.test.ts +++ b/packages/backend/src/tokens/__tests__/verify.test.ts @@ -120,10 +120,8 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { const data = result.data as MachineToken; expect(data.id).toBe('m2m_ey966f1b1xf93586b2debdcadb0b3bd1'); - expect(data.name).toBe('my-machine-token'); expect(data.subject).toBe('mch_2vYVtestTESTtestTESTtestTESTtest'); expect(data.scopes).toEqual(['read:foo', 'write:bar']); - expect(data.claims).toEqual({ foo: 'bar' }); }); it('verifies provided OAuth token', async () => { diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 9e80332b61a..0b354d53937 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -96,8 +96,6 @@ type MachineObjectExtendedProperties = { | { name: string; claims: Claims | null; userId: null; orgId: string } : { name: null; claims: null; userId: null; orgId: null }; machine_token: { - name: TAuthenticated extends true ? string : null; - claims: TAuthenticated extends true ? Claims | null : null; machineId: TAuthenticated extends true ? string : null; }; oauth_token: { @@ -285,8 +283,6 @@ export function authenticatedMachineObject( return { ...baseObject, tokenType, - name: result.name, - claims: result.claims, scopes: result.scopes, machineId: result.subject, } as unknown as AuthenticatedMachineObject; From 201cb235e9ddae5f97e8ca0fd60905803e3f8933 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 28 Jul 2025 16:27:17 -0700 Subject: [PATCH 22/51] fix tests --- ...ineTokenApi.ts => MachineTokenApi.test.ts} | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) rename packages/backend/src/api/__tests__/{MachineTokenApi.ts => MachineTokenApi.test.ts} (67%) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts similarity index 67% rename from packages/backend/src/api/__tests__/MachineTokenApi.ts rename to packages/backend/src/api/__tests__/MachineTokenApi.test.ts index d6733cf871d..8cf80646a06 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -19,11 +19,11 @@ describe('MachineTokenAPI', () => { scopes: [], secret: m2mSecret, revoked: false, - revocationReason: null, + revocation_reason: null, expired: false, expiration: 1753746916590, - createdAt: 1753743316590, - updatedAt: 1753743316590, + created_at: 1753743316590, + updated_at: 1753743316590, }; it('creates a machine-to-machine token', async () => { @@ -33,8 +33,8 @@ describe('MachineTokenAPI', () => { }; server.use( - http.get( - `https://api.clerk.test/m2m_tokens`, + http.post( + 'https://api.clerk.test/m2m_tokens', validateHeaders(() => { return HttpResponse.json(mockM2MToken); }), @@ -47,25 +47,52 @@ describe('MachineTokenAPI', () => { expect(response.secret).toBe(m2mSecret); }); + it('handles missing machine secret', async () => { + server.use( + http.get( + `https://api.clerk.test/m2m_tokens`, + validateHeaders(() => { + return HttpResponse.json(mockM2MToken); + }), + ), + ); + + // @ts-expect-error - machineSecret is required + const response = await apiClient.machineTokens.create({}).catch(err => err); + + expect(response.message).toBe('A machine secret is required.'); + }); + it('revokes a machine-to-machine token', async () => { const revokeParams = { machineSecret: 'ak_xxxxx', m2mTokenId: m2mId, + revocationReason: 'revoked by test', + }; + + const mockRevokedM2MToken = { + object: 'machine_to_machine_token', + id: m2mId, + subject: 'mch_xxxxx', + scopes: [], + revoked: true, + revocation_reason: 'revoked by test', + expired: false, + expiration: 1753746916590, + created_at: 1753743316590, + updated_at: 1753743316590, }; server.use( http.post(`https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, () => { - return HttpResponse.json({ - ...mockM2MToken, - revoked: true, - revocationReason: 'revoked by test', - }); + return HttpResponse.json(mockRevokedM2MToken); }), ); const response = await apiClient.machineTokens.revoke(revokeParams); expect(response.revoked).toBe(true); + expect(response.secret).toBeUndefined(); expect(response.revocationReason).toBe('revoked by test'); }); From 64afde60cc577fb7d7cecf8effe96051277babd1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 28 Jul 2025 16:39:09 -0700 Subject: [PATCH 23/51] fix incorrect method in tests --- packages/backend/src/api/__tests__/MachineTokenApi.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index 8cf80646a06..19f729ef2fa 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -35,7 +35,8 @@ describe('MachineTokenAPI', () => { server.use( http.post( 'https://api.clerk.test/m2m_tokens', - validateHeaders(() => { + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); return HttpResponse.json(mockM2MToken); }), ), @@ -49,7 +50,7 @@ describe('MachineTokenAPI', () => { it('handles missing machine secret', async () => { server.use( - http.get( + http.post( `https://api.clerk.test/m2m_tokens`, validateHeaders(() => { return HttpResponse.json(mockM2MToken); @@ -99,7 +100,6 @@ describe('MachineTokenAPI', () => { it('verifies a machine-to-machine token', async () => { const verifyParams = { machineSecret: 'ak_xxxxx', - m2mTokenId: m2mId, secret: m2mSecret, }; From b38465bf43218f5213d25e8e630c58feb5d18376 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 08:29:39 -0700 Subject: [PATCH 24/51] chore: update tests --- .../src/api/__tests__/MachineTokenApi.test.ts | 272 ++++++++++++++---- 1 file changed, 209 insertions(+), 63 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index 19f729ef2fa..1989b2be0c0 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -5,10 +5,6 @@ import { server, validateHeaders } from '../../mock-server'; import { createBackendApiClient } from '../factory'; describe('MachineTokenAPI', () => { - const apiClient = createBackendApiClient({ - apiUrl: 'https://api.clerk.test', - }); - const m2mId = 'mt_xxxxx'; const m2mSecret = 'mt_secret_xxxxx'; @@ -26,51 +22,57 @@ describe('MachineTokenAPI', () => { updated_at: 1753743316590, }; - it('creates a machine-to-machine token', async () => { - const createParams = { - machineSecret: 'ak_xxxxx', - secondsUntilExpiration: 3600, - }; + describe('create', () => { + it('accepts a machine secret as authorization header', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); - server.use( - http.post( - 'https://api.clerk.test/m2m_tokens', - validateHeaders(({ request }) => { - expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); - return HttpResponse.json(mockM2MToken); - }), - ), - ); + const createParams = { + machineSecret: 'ak_xxxxx', + secondsUntilExpiration: 3600, + }; - const response = await apiClient.machineTokens.create(createParams); + server.use( + http.post( + 'https://api.clerk.test/m2m_tokens', + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); + return HttpResponse.json(mockM2MToken); + }), + ), + ); - expect(response.id).toBe(m2mId); - expect(response.secret).toBe(m2mSecret); - }); + const response = await apiClient.machineTokens.create(createParams); - it('handles missing machine secret', async () => { - server.use( - http.post( - `https://api.clerk.test/m2m_tokens`, - validateHeaders(() => { - return HttpResponse.json(mockM2MToken); - }), - ), - ); + expect(response.id).toBe(m2mId); + expect(response.secret).toBe(m2mSecret); + }); - // @ts-expect-error - machineSecret is required - const response = await apiClient.machineTokens.create({}).catch(err => err); + it('does not accept an instance secret as authorization header', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + secretKey: 'sk_xxxxx', + }); - expect(response.message).toBe('A machine secret is required.'); - }); + server.use( + http.post( + 'https://api.clerk.test/m2m_tokens', + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer sk_xxxxx'); + return HttpResponse.json(mockM2MToken); + }), + ), + ); - it('revokes a machine-to-machine token', async () => { - const revokeParams = { - machineSecret: 'ak_xxxxx', - m2mTokenId: m2mId, - revocationReason: 'revoked by test', - }; + // @ts-expect-error - machineSecret is required + const response = await apiClient.machineTokens.create({}).catch(err => err); + + expect(response.message).toBe('A machine secret is required.'); + }); + }); + describe('revoke', () => { const mockRevokedM2MToken = { object: 'machine_to_machine_token', id: m2mId, @@ -84,34 +86,178 @@ describe('MachineTokenAPI', () => { updated_at: 1753743316590, }; - server.use( - http.post(`https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, () => { - return HttpResponse.json(mockRevokedM2MToken); - }), - ); + it('accepts a machine secret as authorization header', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + const revokeParams = { + machineSecret: 'ak_xxxxx', + m2mTokenId: m2mId, + revocationReason: 'revoked by test', + }; + + server.use( + http.post(`https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, () => { + return HttpResponse.json(mockRevokedM2MToken); + }), + ); + + const response = await apiClient.machineTokens.revoke(revokeParams); + + expect(response.revoked).toBe(true); + expect(response.secret).toBeUndefined(); + expect(response.revocationReason).toBe('revoked by test'); + }); + + it('accepts an instance secret as authorization header', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + secretKey: 'sk_xxxxx', + }); + + const revokeParams = { + m2mTokenId: m2mId, + revocationReason: 'revoked by test', + }; + + const mockRevokedM2MToken = { + object: 'machine_to_machine_token', + id: m2mId, + subject: 'mch_xxxxx', + scopes: [], + revoked: true, + revocation_reason: 'revoked by test', + expired: false, + expiration: 1753746916590, + created_at: 1753743316590, + updated_at: 1753743316590, + }; + + server.use( + http.post(`https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, () => { + return HttpResponse.json(mockRevokedM2MToken); + }), + ); + + const response = await apiClient.machineTokens.revoke(revokeParams); + + expect(response.revoked).toBe(true); + expect(response.secret).toBeUndefined(); + expect(response.revocationReason).toBe('revoked by test'); + }); + + it('requires a machine secret or instance secret', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); - const response = await apiClient.machineTokens.revoke(revokeParams); + const revokeParams = { + m2mTokenId: m2mId, + revocationReason: 'revoked by test', + }; - expect(response.revoked).toBe(true); - expect(response.secret).toBeUndefined(); - expect(response.revocationReason).toBe('revoked by test'); + server.use( + http.post( + `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBeNull(); + return HttpResponse.json( + { + errors: [ + { + code: 'authorization_header_format_invalid', + message: 'Invalid Authorization header format', + long_message: `Invalid Authorization header format. Must be "Bearer "`, + }, + ], + }, + { status: 401 }, + ); + }), + ), + ); + + const errResponse = await apiClient.machineTokens.revoke(revokeParams).catch(err => err); + + expect(errResponse.status).toBe(401); + }); }); - it('verifies a machine-to-machine token', async () => { - const verifyParams = { - machineSecret: 'ak_xxxxx', - secret: m2mSecret, - }; + describe('verifySecret', () => { + it('accepts a machine secret as authorization header', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + const verifyParams = { + machineSecret: 'ak_xxxxx', + secret: m2mSecret, + }; + + server.use( + http.post('https://api.clerk.test/m2m_tokens/verify', () => { + return HttpResponse.json(mockM2MToken); + }), + ); + + const response = await apiClient.machineTokens.verifySecret(verifyParams); - server.use( - http.post('https://api.clerk.test/m2m_tokens/verify', () => { - return HttpResponse.json(mockM2MToken); - }), - ); + expect(response.id).toBe(m2mId); + expect(response.secret).toBe(m2mSecret); + }); + + it('accepts an instance secret as authorization header', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + secretKey: 'sk_xxxxx', + }); + + const verifyParams = { + secret: m2mSecret, + }; + + server.use( + http.post('https://api.clerk.test/m2m_tokens/verify', () => { + return HttpResponse.json(mockM2MToken); + }), + ); + + const response = await apiClient.machineTokens.verifySecret(verifyParams); + + expect(response.id).toBe(m2mId); + expect(response.secret).toBe(m2mSecret); + }); + + it('requires a machine secret or instance secret', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + const verifyParams = { + secret: m2mSecret, + }; + + server.use( + http.post('https://api.clerk.test/m2m_tokens/verify', () => { + return HttpResponse.json( + { + errors: [ + { + code: 'authorization_header_format_invalid', + message: 'Invalid Authorization header format', + long_message: `Invalid Authorization header format. Must be "Bearer "`, + }, + ], + }, + { status: 401 }, + ); + }), + ); - const response = await apiClient.machineTokens.verifySecret(verifyParams); + const errResponse = await apiClient.machineTokens.verifySecret(verifyParams).catch(err => err); - expect(response.id).toBe(m2mId); - expect(response.secret).toBe(m2mSecret); + expect(errResponse.status).toBe(401); + }); }); }); From 18b76da9dfe419cace3d2e1d92fcd9e1cdd8339e Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 09:00:52 -0700 Subject: [PATCH 25/51] chore: update test descriptions --- .../src/api/__tests__/MachineTokenApi.test.ts | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index 1989b2be0c0..6b7d4b0997f 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -23,7 +23,7 @@ describe('MachineTokenAPI', () => { }; describe('create', () => { - it('accepts a machine secret as authorization header', async () => { + it('creates a m2m token using machine secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', }); @@ -58,8 +58,7 @@ describe('MachineTokenAPI', () => { server.use( http.post( 'https://api.clerk.test/m2m_tokens', - validateHeaders(({ request }) => { - expect(request.headers.get('Authorization')).toBe('Bearer sk_xxxxx'); + validateHeaders(() => { return HttpResponse.json(mockM2MToken); }), ), @@ -86,7 +85,7 @@ describe('MachineTokenAPI', () => { updated_at: 1753743316590, }; - it('accepts a machine secret as authorization header', async () => { + it('revokes a m2m token using machine secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', }); @@ -98,9 +97,13 @@ describe('MachineTokenAPI', () => { }; server.use( - http.post(`https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, () => { - return HttpResponse.json(mockRevokedM2MToken); - }), + http.post( + `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); + return HttpResponse.json(mockRevokedM2MToken); + }), + ), ); const response = await apiClient.machineTokens.revoke(revokeParams); @@ -110,7 +113,7 @@ describe('MachineTokenAPI', () => { expect(response.revocationReason).toBe('revoked by test'); }); - it('accepts an instance secret as authorization header', async () => { + it('revokes a m2m token using instance secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', secretKey: 'sk_xxxxx', @@ -135,9 +138,13 @@ describe('MachineTokenAPI', () => { }; server.use( - http.post(`https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, () => { - return HttpResponse.json(mockRevokedM2MToken); - }), + http.post( + `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer sk_xxxxx'); + return HttpResponse.json(mockRevokedM2MToken); + }), + ), ); const response = await apiClient.machineTokens.revoke(revokeParams); @@ -147,7 +154,7 @@ describe('MachineTokenAPI', () => { expect(response.revocationReason).toBe('revoked by test'); }); - it('requires a machine secret or instance secret', async () => { + it('requires a machine secret or instance secret to revoke a m2m token', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', }); @@ -185,7 +192,7 @@ describe('MachineTokenAPI', () => { }); describe('verifySecret', () => { - it('accepts a machine secret as authorization header', async () => { + it('verifies a m2m token using machine secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', }); @@ -196,9 +203,13 @@ describe('MachineTokenAPI', () => { }; server.use( - http.post('https://api.clerk.test/m2m_tokens/verify', () => { - return HttpResponse.json(mockM2MToken); - }), + http.post( + 'https://api.clerk.test/m2m_tokens/verify', + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); + return HttpResponse.json(mockM2MToken); + }), + ), ); const response = await apiClient.machineTokens.verifySecret(verifyParams); @@ -207,7 +218,7 @@ describe('MachineTokenAPI', () => { expect(response.secret).toBe(m2mSecret); }); - it('accepts an instance secret as authorization header', async () => { + it('verifies a m2m token using instance secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', secretKey: 'sk_xxxxx', @@ -218,9 +229,13 @@ describe('MachineTokenAPI', () => { }; server.use( - http.post('https://api.clerk.test/m2m_tokens/verify', () => { - return HttpResponse.json(mockM2MToken); - }), + http.post( + 'https://api.clerk.test/m2m_tokens/verify', + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer sk_xxxxx'); + return HttpResponse.json(mockM2MToken); + }), + ), ); const response = await apiClient.machineTokens.verifySecret(verifyParams); @@ -229,7 +244,7 @@ describe('MachineTokenAPI', () => { expect(response.secret).toBe(m2mSecret); }); - it('requires a machine secret or instance secret', async () => { + it('requires a machine secret or instance secret to verify a m2m token', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', }); @@ -239,20 +254,24 @@ describe('MachineTokenAPI', () => { }; server.use( - http.post('https://api.clerk.test/m2m_tokens/verify', () => { - return HttpResponse.json( - { - errors: [ - { - code: 'authorization_header_format_invalid', - message: 'Invalid Authorization header format', - long_message: `Invalid Authorization header format. Must be "Bearer "`, - }, - ], - }, - { status: 401 }, - ); - }), + http.post( + 'https://api.clerk.test/m2m_tokens/verify', + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBeNull(); + return HttpResponse.json( + { + errors: [ + { + code: 'authorization_header_format_invalid', + message: 'Invalid Authorization header format', + long_message: `Invalid Authorization header format. Must be "Bearer "`, + }, + ], + }, + { status: 401 }, + ); + }), + ), ); const errResponse = await apiClient.machineTokens.verifySecret(verifyParams).catch(err => err); From d91404cbb6c627fdfd3d9e980d8f538fa1ef2151 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 09:11:50 -0700 Subject: [PATCH 26/51] chore: improve tests --- .../src/api/__tests__/MachineTokenApi.test.ts | 45 ++----------------- 1 file changed, 4 insertions(+), 41 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index 6b7d4b0997f..dd86ea05cca 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -124,19 +124,6 @@ describe('MachineTokenAPI', () => { revocationReason: 'revoked by test', }; - const mockRevokedM2MToken = { - object: 'machine_to_machine_token', - id: m2mId, - subject: 'mch_xxxxx', - scopes: [], - revoked: true, - revocation_reason: 'revoked by test', - expired: false, - expiration: 1753746916590, - created_at: 1753743316590, - updated_at: 1753743316590, - }; - server.use( http.post( `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, @@ -167,20 +154,8 @@ describe('MachineTokenAPI', () => { server.use( http.post( `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, - validateHeaders(({ request }) => { - expect(request.headers.get('Authorization')).toBeNull(); - return HttpResponse.json( - { - errors: [ - { - code: 'authorization_header_format_invalid', - message: 'Invalid Authorization header format', - long_message: `Invalid Authorization header format. Must be "Bearer "`, - }, - ], - }, - { status: 401 }, - ); + validateHeaders(() => { + return HttpResponse.json(mockRevokedM2MToken); }), ), ); @@ -256,20 +231,8 @@ describe('MachineTokenAPI', () => { server.use( http.post( 'https://api.clerk.test/m2m_tokens/verify', - validateHeaders(({ request }) => { - expect(request.headers.get('Authorization')).toBeNull(); - return HttpResponse.json( - { - errors: [ - { - code: 'authorization_header_format_invalid', - message: 'Invalid Authorization header format', - long_message: `Invalid Authorization header format. Must be "Bearer "`, - }, - ], - }, - { status: 401 }, - ); + validateHeaders(() => { + return HttpResponse.json(mockM2MToken); }), ), ); From 37a3d65e6c3cf121b88c96f01e4f617fd772f092 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 09:37:02 -0700 Subject: [PATCH 27/51] chore: update changeset --- .changeset/hot-tables-worry.md | 54 +++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/.changeset/hot-tables-worry.md b/.changeset/hot-tables-worry.md index 2637253987a..db0f8b8d34f 100644 --- a/.changeset/hot-tables-worry.md +++ b/.changeset/hot-tables-worry.md @@ -2,4 +2,56 @@ "@clerk/backend": minor --- -WIP M2M Tokens +Adds machine-to-machine endpoints to the Backend SDK: + +### Create M2M Tokens + +A machine secret is required when creating M2M tokens. + +```ts +const clerkClient = createClerkClient() + +clerkClient.machineTokens.create({ + machineSecret: 'ak_xxxxx', +}) +``` + +### Revoke M2M Tokens + +You can revoke tokens using either a machine secret or instance secret: + +```ts +// Using machine secret +const clerkClient = createClerkClient() +clerkClient.machineTokens.revoke({ + machineSecret: 'ak_xxxxx', + m2mTokenId: 'mt_xxxxx', + revocationReason: 'Revoked by user', +}) + +// Using instance secret (default) +const clerkClient = createClerkClient({ secretKey: 'sk_xxxx' }) +clerkClient.machineTokens.revoke({ + m2mTokenId: 'mt_xxxxx', + revocationReason: 'Revoked by user', +}) +``` + +### Verify M2M Tokens + +You can verify tokens using either a machine secret or instance secret: + +```ts +// Using machine secret +const clerkClient = createClerkClient() +clerkClient.machineTokens.verifySecret({ + machineSecret: 'ak_xxxxx', + secret: 'mt_secret_xxxxx', +}) + +// Using instance secret (default) +const clerkClient = createClerkClient({ secretKey: 'sk_xxxx' }) +clerkClient.machineTokens.verifySecret({ + secret: 'mt_secret_xxxxx', +}) +``` \ No newline at end of file From 1d69db81daa156b664c7a3e1399fe45a63526789 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 09:58:07 -0700 Subject: [PATCH 28/51] chore: skip pub key init for machine tokens --- packages/backend/src/tokens/authenticateContext.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index 3c0193b6fe9..01e80b68e6f 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -69,7 +69,9 @@ class AuthenticateContext implements AuthenticateContext { // Even though the options are assigned to this later in this function // we set the publishableKey here because it is being used in cookies/headers/handshake-values // as part of getMultipleAppsCookie - this.initPublishableKeyValues(options); + if (options.acceptsToken !== 'machine_token') { + this.initPublishableKeyValues(options); + } this.initHeaderValues(); // initCookieValues should be used before initHandshakeValues because it depends on suffixedCookies this.initCookieValues(); From b26bd7602740d89daa08085b9850a54ca59c3647 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 10:08:25 -0700 Subject: [PATCH 29/51] chore: skip pub and secret key check for authenticate request with machine tokens --- .../backend/src/tokens/authenticateContext.ts | 3 ++- packages/backend/src/tokens/request.ts | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index 01e80b68e6f..7d07cd83efd 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -6,6 +6,7 @@ import { runtime } from '../runtime'; import { assertValidPublishableKey } from '../util/optionsAssertions'; import { getCookieSuffix, getSuffixedCookieName, parsePublishableKey } from '../util/shared'; import type { ClerkRequest } from './clerkRequest'; +import { TokenType } from './tokenTypes'; import type { AuthenticateRequestOptions } from './types'; interface AuthenticateContext extends AuthenticateRequestOptions { @@ -69,7 +70,7 @@ class AuthenticateContext implements AuthenticateContext { // Even though the options are assigned to this later in this function // we set the publishableKey here because it is being used in cookies/headers/handshake-values // as part of getMultipleAppsCookie - if (options.acceptsToken !== 'machine_token') { + if (options.acceptsToken !== TokenType.MachineToken) { this.initPublishableKeyValues(options); } this.initHeaderValues(); diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index a8064934a8e..5d5a03f51d2 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -139,17 +139,20 @@ export const authenticateRequest: AuthenticateRequest = (async ( options: AuthenticateRequestOptions, ): Promise | UnauthenticatedState> => { const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options); - assertValidSecretKey(authenticateContext.secretKey); // Default tokenType is session_token for backwards compatibility. const acceptsToken = options.acceptsToken ?? TokenType.SessionToken; - if (authenticateContext.isSatellite) { - assertSignInUrlExists(authenticateContext.signInUrl, authenticateContext.secretKey); - if (authenticateContext.signInUrl && authenticateContext.origin) { - assertSignInUrlFormatAndOrigin(authenticateContext.signInUrl, authenticateContext.origin); + if (acceptsToken !== TokenType.MachineToken) { + assertValidSecretKey(authenticateContext.secretKey); + + if (authenticateContext.isSatellite) { + assertSignInUrlExists(authenticateContext.signInUrl, authenticateContext.secretKey); + if (authenticateContext.signInUrl && authenticateContext.origin) { + assertSignInUrlFormatAndOrigin(authenticateContext.signInUrl, authenticateContext.origin); + } + assertProxyUrlOrDomain(authenticateContext.proxyUrl || authenticateContext.domain); } - assertProxyUrlOrDomain(authenticateContext.proxyUrl || authenticateContext.domain); } const organizationMatcher = new OrganizationMatcher(options.organizationSyncOptions); From d6092853c88765fe4afe179e6f2abde58868ce02 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 10:58:45 -0700 Subject: [PATCH 30/51] fix error handling --- packages/backend/src/tokens/request.ts | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 5d5a03f51d2..4974f5a4b5e 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -79,8 +79,9 @@ function checkTokenTypeMismatch( ): UnauthenticatedState | null { const mismatch = !isTokenTypeAccepted(parsedTokenType, acceptsToken); if (mismatch) { + const tokenTypeToReturn = (typeof acceptsToken === 'string' ? acceptsToken : parsedTokenType) as MachineTokenType; return signedOut({ - tokenType: parsedTokenType, + tokenType: tokenTypeToReturn, authenticateContext, reason: AuthErrorReason.TokenTypeMismatch, }); @@ -143,16 +144,20 @@ export const authenticateRequest: AuthenticateRequest = (async ( // Default tokenType is session_token for backwards compatibility. const acceptsToken = options.acceptsToken ?? TokenType.SessionToken; - if (acceptsToken !== TokenType.MachineToken) { - assertValidSecretKey(authenticateContext.secretKey); + // Machine tokens are header-only and don't require session-based validation + // (secret key, publishable key, cookies, handshake flows, etc.) + if (acceptsToken === TokenType.MachineToken) { + return authenticateMachineRequestWithTokenInHeader(); + } - if (authenticateContext.isSatellite) { - assertSignInUrlExists(authenticateContext.signInUrl, authenticateContext.secretKey); - if (authenticateContext.signInUrl && authenticateContext.origin) { - assertSignInUrlFormatAndOrigin(authenticateContext.signInUrl, authenticateContext.origin); - } - assertProxyUrlOrDomain(authenticateContext.proxyUrl || authenticateContext.domain); + assertValidSecretKey(authenticateContext.secretKey); + + if (authenticateContext.isSatellite) { + assertSignInUrlExists(authenticateContext.signInUrl, authenticateContext.secretKey); + if (authenticateContext.signInUrl && authenticateContext.origin) { + assertSignInUrlFormatAndOrigin(authenticateContext.signInUrl, authenticateContext.origin); } + assertProxyUrlOrDomain(authenticateContext.proxyUrl || authenticateContext.domain); } const organizationMatcher = new OrganizationMatcher(options.organizationSyncOptions); @@ -772,11 +777,7 @@ export const authenticateRequest: AuthenticateRequest = (async ( } // Machine requests cannot have the token in the cookie, it must be in header. - if ( - acceptsToken === TokenType.OAuthToken || - acceptsToken === TokenType.ApiKey || - acceptsToken === TokenType.MachineToken - ) { + if (acceptsToken === TokenType.OAuthToken || acceptsToken === TokenType.ApiKey) { return signedOut({ tokenType: acceptsToken, authenticateContext, From 2e080db3dd3e70007a39e382261f3c8a34935c01 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 13:14:55 -0700 Subject: [PATCH 31/51] chore: allow machine secrets in authenticateRequest --- .../src/api/__tests__/MachineTokenApi.test.ts | 2 +- .../src/api/endpoints/MachineTokenApi.ts | 2 +- packages/backend/src/api/resources/JSON.ts | 1 + .../backend/src/api/resources/MachineToken.ts | 2 + packages/backend/src/fixtures/machine.ts | 4 +- .../src/tokens/__tests__/authObjects.test.ts | 2 +- .../src/tokens/__tests__/request.test.ts | 29 +++++++++++--- .../src/tokens/__tests__/verify.test.ts | 35 +++++++++++++++- packages/backend/src/tokens/authObjects.ts | 3 +- .../backend/src/tokens/authenticateContext.ts | 3 +- packages/backend/src/tokens/request.ts | 40 +++++++++++++------ packages/backend/src/tokens/types.ts | 6 +++ packages/backend/src/tokens/verify.ts | 9 +++-- 13 files changed, 106 insertions(+), 32 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index dd86ea05cca..a3ba28a2d09 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -67,7 +67,7 @@ describe('MachineTokenAPI', () => { // @ts-expect-error - machineSecret is required const response = await apiClient.machineTokens.create({}).catch(err => err); - expect(response.message).toBe('A machine secret is required.'); + expect(response.message).toBe('Missing machine secret.'); }); }); diff --git a/packages/backend/src/api/endpoints/MachineTokenApi.ts b/packages/backend/src/api/endpoints/MachineTokenApi.ts index 07f7da912f8..e1822819329 100644 --- a/packages/backend/src/api/endpoints/MachineTokenApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokenApi.ts @@ -24,7 +24,7 @@ type VerifyMachineTokenParams = { export class MachineTokenApi extends AbstractAPI { #requireMachineSecret(machineSecret?: string | null): asserts machineSecret is string { if (!machineSecret) { - throw new Error('A machine secret is required.'); + throw new Error('Missing machine secret.'); } } diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 49fa8aacda5..87bc0d1a379 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -734,6 +734,7 @@ export interface MachineTokenJSON extends ClerkResourceJSON { secret?: string; subject: string; scopes: string[]; + claims: Record | null; revoked: boolean; revocation_reason: string | null; expired: boolean; diff --git a/packages/backend/src/api/resources/MachineToken.ts b/packages/backend/src/api/resources/MachineToken.ts index d9076b68555..2ee4b69a69d 100644 --- a/packages/backend/src/api/resources/MachineToken.ts +++ b/packages/backend/src/api/resources/MachineToken.ts @@ -5,6 +5,7 @@ export class MachineToken { readonly id: string, readonly subject: string, readonly scopes: string[], + readonly claims: Record | null, readonly revoked: boolean, readonly revocationReason: string | null, readonly expired: boolean, @@ -19,6 +20,7 @@ export class MachineToken { data.id, data.subject, data.scopes, + data.claims, data.revoked, data.revocation_reason, data.expired, diff --git a/packages/backend/src/fixtures/machine.ts b/packages/backend/src/fixtures/machine.ts index b16dd04e5a3..0372853f2e3 100644 --- a/packages/backend/src/fixtures/machine.ts +++ b/packages/backend/src/fixtures/machine.ts @@ -38,15 +38,13 @@ export const mockVerificationResults = { }, machine_token: { id: 'm2m_ey966f1b1xf93586b2debdcadb0b3bd1', - name: 'my-machine-token', subject: 'mch_2vYVtestTESTtestTESTtestTESTtest', - scopes: ['read:foo', 'write:bar'], + scopes: ['mch_1xxxxx', 'mch_2xxxxx'], claims: { foo: 'bar' }, revoked: false, revocationReason: null, expired: false, expiration: null, - createdBy: null, creationReason: null, createdAt: 1745185445567, updatedAt: 1745185445567, diff --git a/packages/backend/src/tokens/__tests__/authObjects.test.ts b/packages/backend/src/tokens/__tests__/authObjects.test.ts index 8006decd716..1f50379a2cb 100644 --- a/packages/backend/src/tokens/__tests__/authObjects.test.ts +++ b/packages/backend/src/tokens/__tests__/authObjects.test.ts @@ -365,7 +365,7 @@ describe('authenticatedMachineObject', () => { expect(authObject.tokenType).toBe('machine_token'); expect(authObject.id).toBe('m2m_ey966f1b1xf93586b2debdcadb0b3bd1'); expect(authObject.subject).toBe('mch_2vYVtestTESTtestTESTtestTESTtest'); - expect(authObject.scopes).toEqual(['read:foo', 'write:bar']); + expect(authObject.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); expect(authObject.machineId).toBe('mch_2vYVtestTESTtestTESTtestTESTtest'); }); }); diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index a2467f845f7..46750a861b6 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -1242,6 +1242,23 @@ describe('tokens.authenticateRequest(options)', () => { }); }); + test('accepts machine secret when verifying machine-to-machine token', async () => { + server.use( + http.post(mockMachineAuthResponses.machine_token.endpoint, ({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); + return HttpResponse.json(mockVerificationResults.machine_token); + }), + ); + + const request = mockRequest({ authorization: `Bearer ${mockTokens.machine_token}` }); + const requestState = await authenticateRequest( + request, + mockOptions({ acceptsToken: 'machine_token', machineSecret: 'ak_xxxxx' }), + ); + + expect(requestState).toBeMachineAuthenticated(); + }); + describe('Any Token Type Authentication', () => { test.each(tokenTypes)('accepts %s when acceptsToken is "any"', async tokenType => { const mockToken = mockTokens[tokenType]; @@ -1281,12 +1298,12 @@ describe('tokens.authenticateRequest(options)', () => { const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'oauth_token' })); expect(result).toBeMachineUnauthenticated({ - tokenType: 'api_key', + tokenType: 'oauth_token', reason: AuthErrorReason.TokenTypeMismatch, message: '', }); expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({ - tokenType: 'api_key', + tokenType: 'oauth_token', isAuthenticated: false, }); }); @@ -1296,12 +1313,12 @@ describe('tokens.authenticateRequest(options)', () => { const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'machine_token' })); expect(result).toBeMachineUnauthenticated({ - tokenType: 'oauth_token', + tokenType: 'machine_token', reason: AuthErrorReason.TokenTypeMismatch, message: '', }); expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({ - tokenType: 'oauth_token', + tokenType: 'machine_token', isAuthenticated: false, }); }); @@ -1311,12 +1328,12 @@ describe('tokens.authenticateRequest(options)', () => { const result = await authenticateRequest(request, mockOptions({ acceptsToken: 'api_key' })); expect(result).toBeMachineUnauthenticated({ - tokenType: 'machine_token', + tokenType: 'api_key', reason: AuthErrorReason.TokenTypeMismatch, message: '', }); expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({ - tokenType: 'machine_token', + tokenType: 'api_key', isAuthenticated: false, }); }); diff --git a/packages/backend/src/tokens/__tests__/verify.test.ts b/packages/backend/src/tokens/__tests__/verify.test.ts index b0f12ff9eb3..a844f69b638 100644 --- a/packages/backend/src/tokens/__tests__/verify.test.ts +++ b/packages/backend/src/tokens/__tests__/verify.test.ts @@ -97,7 +97,7 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { expect(data.claims).toEqual({ foo: 'bar' }); }); - it('verifies provided Machine token', async () => { + it('verifies provided Machine token with instance secret key', async () => { const token = 'mt_8XOIucKvqHVr5tYP123456789abcdefghij'; server.use( @@ -121,7 +121,38 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { const data = result.data as MachineToken; expect(data.id).toBe('m2m_ey966f1b1xf93586b2debdcadb0b3bd1'); expect(data.subject).toBe('mch_2vYVtestTESTtestTESTtestTESTtest'); - expect(data.scopes).toEqual(['read:foo', 'write:bar']); + expect(data.claims).toEqual({ foo: 'bar' }); + expect(data.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); + }); + + it('verifies provided Machine token with machine secret', async () => { + const token = 'mt_8XOIucKvqHVr5tYP123456789abcdefghij'; + + server.use( + http.post( + 'https://api.clerk.test/m2m_tokens/verify', + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); + return HttpResponse.json(mockVerificationResults.machine_token); + }), + ), + ); + + const result = await verifyMachineAuthToken(token, { + apiUrl: 'https://api.clerk.test', + // @ts-expect-error - machineSecret from options + machineSecret: 'ak_xxxxx', + }); + + expect(result.tokenType).toBe('machine_token'); + expect(result.data).toBeDefined(); + expect(result.errors).toBeUndefined(); + + const data = result.data as MachineToken; + expect(data.id).toBe('m2m_ey966f1b1xf93586b2debdcadb0b3bd1'); + expect(data.subject).toBe('mch_2vYVtestTESTtestTESTtestTESTtest'); + expect(data.claims).toEqual({ foo: 'bar' }); + expect(data.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); }); it('verifies provided OAuth token', async () => { diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 0b354d53937..79cc800a00b 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -96,6 +96,7 @@ type MachineObjectExtendedProperties = { | { name: string; claims: Claims | null; userId: null; orgId: string } : { name: null; claims: null; userId: null; orgId: null }; machine_token: { + claims: TAuthenticated extends true ? Claims | null : null; machineId: TAuthenticated extends true ? string : null; }; oauth_token: { @@ -283,6 +284,7 @@ export function authenticatedMachineObject( return { ...baseObject, tokenType, + claims: result.claims, scopes: result.scopes, machineId: result.subject, } as unknown as AuthenticatedMachineObject; @@ -335,7 +337,6 @@ export function unauthenticatedMachineObject( return { ...baseObject, tokenType, - name: null, claims: null, scopes: null, machineId: null, diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index 7d07cd83efd..b6a37d942bc 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -69,7 +69,8 @@ class AuthenticateContext implements AuthenticateContext { ) { // Even though the options are assigned to this later in this function // we set the publishableKey here because it is being used in cookies/headers/handshake-values - // as part of getMultipleAppsCookie + // as part of getMultipleAppsCookie. + // Machine tokens don't require publishable keys. if (options.acceptsToken !== TokenType.MachineToken) { this.initPublishableKeyValues(options); } diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 4974f5a4b5e..9640ad5806f 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -60,6 +60,14 @@ function assertSignInUrlFormatAndOrigin(_signInUrl: string, origin: string) { } } +function assertMachineSecretOrSecretKey(authenticateContext: AuthenticateContext) { + if (!authenticateContext.machineSecret && !authenticateContext.secretKey) { + throw new Error( + 'Machine token authentication requires either a machine secret or a Clerk secret key. ' + + 'Provide either the `machineSecret` option or ensure Clerk secret key is set.', + ); + } +} function isRequestEligibleForRefresh( err: TokenVerificationError, authenticateContext: { refreshTokenInCookie?: string }, @@ -144,20 +152,22 @@ export const authenticateRequest: AuthenticateRequest = (async ( // Default tokenType is session_token for backwards compatibility. const acceptsToken = options.acceptsToken ?? TokenType.SessionToken; - // Machine tokens are header-only and don't require session-based validation - // (secret key, publishable key, cookies, handshake flows, etc.) - if (acceptsToken === TokenType.MachineToken) { - return authenticateMachineRequestWithTokenInHeader(); - } + // machine-to-machine tokens can accept a machine secret or a secret key + if (acceptsToken !== TokenType.MachineToken) { + assertValidSecretKey(authenticateContext.secretKey); - assertValidSecretKey(authenticateContext.secretKey); - - if (authenticateContext.isSatellite) { - assertSignInUrlExists(authenticateContext.signInUrl, authenticateContext.secretKey); - if (authenticateContext.signInUrl && authenticateContext.origin) { - assertSignInUrlFormatAndOrigin(authenticateContext.signInUrl, authenticateContext.origin); + if (authenticateContext.isSatellite) { + assertSignInUrlExists(authenticateContext.signInUrl, authenticateContext.secretKey); + if (authenticateContext.signInUrl && authenticateContext.origin) { + assertSignInUrlFormatAndOrigin(authenticateContext.signInUrl, authenticateContext.origin); + } + assertProxyUrlOrDomain(authenticateContext.proxyUrl || authenticateContext.domain); } - assertProxyUrlOrDomain(authenticateContext.proxyUrl || authenticateContext.domain); + } + + // Make sure a machine secret or instance secret key is provided if acceptsToken is machine_token + if (acceptsToken === TokenType.MachineToken) { + assertMachineSecretOrSecretKey(authenticateContext); } const organizationMatcher = new OrganizationMatcher(options.organizationSyncOptions); @@ -777,7 +787,11 @@ export const authenticateRequest: AuthenticateRequest = (async ( } // Machine requests cannot have the token in the cookie, it must be in header. - if (acceptsToken === TokenType.OAuthToken || acceptsToken === TokenType.ApiKey) { + if ( + acceptsToken === TokenType.OAuthToken || + acceptsToken === TokenType.ApiKey || + acceptsToken === TokenType.MachineToken + ) { return signedOut({ tokenType: acceptsToken, authenticateContext, diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts index 2b95dfb6c23..29d8da6b83e 100644 --- a/packages/backend/src/tokens/types.ts +++ b/packages/backend/src/tokens/types.ts @@ -67,6 +67,12 @@ export type AuthenticateRequestOptions = { * @default 'session_token' */ acceptsToken?: TokenType | TokenType[] | 'any'; + /** + * The machine secret to use when verifying machine-to-machine tokens. + * This will override the instance secret key. + * @internal + */ + machineSecret?: string; } & VerifyTokenOptions; /** diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index 79cc31e9176..6ace1a71391 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -200,13 +200,16 @@ function handleClerkAPIError( }; } -async function verifyMachineToken( +export async function verifyMachineToken( secret: string, - options: VerifyTokenOptions, + options: VerifyTokenOptions & { machineSecret?: string }, ): Promise> { try { const client = createBackendApiClient(options); - const verifiedToken = await client.machineTokens.verifySecret({ secret }); + const verifiedToken = await client.machineTokens.verifySecret({ + secret, + machineSecret: options.machineSecret, + }); return { data: verifiedToken, tokenType: TokenType.MachineToken, errors: undefined }; } catch (err: any) { return handleClerkAPIError(TokenType.MachineToken, err, 'Machine token not found'); From 7055b8a244de623a95623b8e5e07f9afa5f27c1d Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 13:18:13 -0700 Subject: [PATCH 32/51] chore: remove unused export keyword --- packages/backend/src/tokens/types.ts | 2 +- packages/backend/src/tokens/verify.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts index 29d8da6b83e..72534c4fe05 100644 --- a/packages/backend/src/tokens/types.ts +++ b/packages/backend/src/tokens/types.ts @@ -69,7 +69,7 @@ export type AuthenticateRequestOptions = { acceptsToken?: TokenType | TokenType[] | 'any'; /** * The machine secret to use when verifying machine-to-machine tokens. - * This will override the instance secret key. + * This will override the Clerk secret key. * @internal */ machineSecret?: string; diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index 6ace1a71391..3533896a5a2 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -200,7 +200,7 @@ function handleClerkAPIError( }; } -export async function verifyMachineToken( +async function verifyMachineToken( secret: string, options: VerifyTokenOptions & { machineSecret?: string }, ): Promise> { From 051dd8584027276cd1a5d1c3f1b1d84d6ad86466 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 13:20:05 -0700 Subject: [PATCH 33/51] chore: more tests --- .../src/api/__tests__/MachineTokenApi.test.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index a3ba28a2d09..d3f2c5225d5 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -12,7 +12,8 @@ describe('MachineTokenAPI', () => { object: 'machine_to_machine_token', id: m2mId, subject: 'mch_xxxxx', - scopes: [], + scopes: ['mch_1xxxxx', 'mch_2xxxxx'], + claims: { foo: 'bar' }, secret: m2mSecret, revoked: false, revocation_reason: null, @@ -47,6 +48,8 @@ describe('MachineTokenAPI', () => { expect(response.id).toBe(m2mId); expect(response.secret).toBe(m2mSecret); + expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); + expect(response.claims).toEqual({ foo: 'bar' }); }); it('does not accept an instance secret as authorization header', async () => { @@ -76,7 +79,8 @@ describe('MachineTokenAPI', () => { object: 'machine_to_machine_token', id: m2mId, subject: 'mch_xxxxx', - scopes: [], + scopes: ['mch_1xxxxx', 'mch_2xxxxx'], + claims: { foo: 'bar' }, revoked: true, revocation_reason: 'revoked by test', expired: false, @@ -111,6 +115,8 @@ describe('MachineTokenAPI', () => { expect(response.revoked).toBe(true); expect(response.secret).toBeUndefined(); expect(response.revocationReason).toBe('revoked by test'); + expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); + expect(response.claims).toEqual({ foo: 'bar' }); }); it('revokes a m2m token using instance secret', async () => { @@ -191,6 +197,8 @@ describe('MachineTokenAPI', () => { expect(response.id).toBe(m2mId); expect(response.secret).toBe(m2mSecret); + expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); + expect(response.claims).toEqual({ foo: 'bar' }); }); it('verifies a m2m token using instance secret', async () => { @@ -217,6 +225,8 @@ describe('MachineTokenAPI', () => { expect(response.id).toBe(m2mId); expect(response.secret).toBe(m2mSecret); + expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); + expect(response.claims).toEqual({ foo: 'bar' }); }); it('requires a machine secret or instance secret to verify a m2m token', async () => { From 7371a32b2908d6585395ea70a90d508f1cd4928f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 29 Jul 2025 13:37:26 -0700 Subject: [PATCH 34/51] chore: add missing secret key or machine secret error test --- .changeset/hot-tables-worry.md | 15 +++++++++++++++ .../backend/src/tokens/__tests__/request.test.ts | 11 +++++++++++ 2 files changed, 26 insertions(+) diff --git a/.changeset/hot-tables-worry.md b/.changeset/hot-tables-worry.md index db0f8b8d34f..4dc86485d8e 100644 --- a/.changeset/hot-tables-worry.md +++ b/.changeset/hot-tables-worry.md @@ -54,4 +54,19 @@ const clerkClient = createClerkClient({ secretKey: 'sk_xxxx' }) clerkClient.machineTokens.verifySecret({ secret: 'mt_secret_xxxxx', }) +``` + +To verify machine-to-machine tokens using when using `authenticateRequest()` with a machine secret, use the `machineSecret` option: + +```ts +const clerkClient = createClerkClient() + +const authReq = await clerkClient.authenticateRequest(c.req.raw, { + acceptsToken: 'machine_token', + machineSecret: 'ak_xxxxx' +}) + +if (authReq.isAuthenticated) { + // ... do something +} ``` \ No newline at end of file diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index 46750a861b6..7237e6338fe 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -1259,6 +1259,17 @@ describe('tokens.authenticateRequest(options)', () => { expect(requestState).toBeMachineAuthenticated(); }); + test('throws an error if acceptsToken is machine_token but machineSecret or secretKey is not provided', async () => { + const request = mockRequest({ authorization: `Bearer ${mockTokens.machine_token}` }); + + await expect( + authenticateRequest(request, mockOptions({ acceptsToken: 'machine_token', secretKey: undefined })), + ).rejects.toThrow( + 'Machine token authentication requires either a machine secret or a Clerk secret key. ' + + 'Provide either the `machineSecret` option or ensure Clerk secret key is set.', + ); + }); + describe('Any Token Type Authentication', () => { test.each(tokenTypes)('accepts %s when acceptsToken is "any"', async tokenType => { const mockToken = mockTokens[tokenType]; From 66423217efcf99573d9ee7331cefc10b7494b074 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 12:39:49 -0700 Subject: [PATCH 35/51] chore: run dedupe --- pnpm-lock.yaml | 131 ------------------------------------------------- 1 file changed, 131 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7c098b38b6..7f24b8faf43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3554,114 +3554,57 @@ packages: engines: {node: '>=18.14.0'} hasBin: true - '@next/env@14.2.30': - resolution: {integrity: sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug==} - '@next/env@14.2.31': resolution: {integrity: sha512-X8VxxYL6VuezrG82h0pUA1V+DuTSJp7Nv15bxq3ivrFqZLjx81rfeHMWOE9T0jm1n3DtHGv8gdn6B0T0kr0D3Q==} - '@next/swc-darwin-arm64@14.2.30': - resolution: {integrity: sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@next/swc-darwin-arm64@14.2.31': resolution: {integrity: sha512-dTHKfaFO/xMJ3kzhXYgf64VtV6MMwDs2viedDOdP+ezd0zWMOQZkxcwOfdcQeQCpouTr9b+xOqMCUXxgLizl8Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.30': - resolution: {integrity: sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@next/swc-darwin-x64@14.2.31': resolution: {integrity: sha512-iSavebQgeMukUAfjfW8Fi2Iz01t95yxRl2w2wCzjD91h5In9la99QIDKcKSYPfqLjCgwz3JpIWxLG6LM/sxL4g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.30': - resolution: {integrity: sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@next/swc-linux-arm64-gnu@14.2.31': resolution: {integrity: sha512-XJb3/LURg1u1SdQoopG6jDL2otxGKChH2UYnUTcby4izjM0il7ylBY5TIA7myhvHj9lG5pn9F2nR2s3i8X9awQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.30': - resolution: {integrity: sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@next/swc-linux-arm64-musl@14.2.31': resolution: {integrity: sha512-IInDAcchNCu3BzocdqdCv1bKCmUVO/bKJHnBFTeq3svfaWpOPewaLJ2Lu3GL4yV76c/86ZvpBbG/JJ1lVIs5MA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.30': - resolution: {integrity: sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-gnu@14.2.31': resolution: {integrity: sha512-YTChJL5/9e4NXPKW+OJzsQa42RiWUNbE+k+ReHvA+lwXk+bvzTsVQboNcezWOuCD+p/J+ntxKOB/81o0MenBhw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.30': - resolution: {integrity: sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-musl@14.2.31': resolution: {integrity: sha512-A0JmD1y4q/9ufOGEAhoa60Sof++X10PEoiWOH0gZ2isufWZeV03NnyRlRmJpRQWGIbRkJUmBo9I3Qz5C10vx4w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.30': - resolution: {integrity: sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - '@next/swc-win32-arm64-msvc@14.2.31': resolution: {integrity: sha512-nowJ5GbMeDOMzbTm29YqrdrD6lTM8qn2wnZfGpYMY7SZODYYpaJHH1FJXE1l1zWICHR+WfIMytlTDBHu10jb8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.30': - resolution: {integrity: sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - '@next/swc-win32-ia32-msvc@14.2.31': resolution: {integrity: sha512-pk9Bu4K0015anTS1OS9d/SpS0UtRObC+xe93fwnm7Gvqbv/W1ZbzhK4nvc96RURIQOux3P/bBH316xz8wjGSsA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.30': - resolution: {integrity: sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@next/swc-win32-x64-msvc@14.2.31': resolution: {integrity: sha512-LwFZd4JFnMHGceItR9+jtlMm8lGLU/IPkgjBBgYmdYSfalbHCiDpjMYtgDQ2wtwiAOSJOCyFI4m8PikrsDyA6Q==} engines: {node: '>= 10'} @@ -11016,24 +10959,6 @@ packages: resolution: {integrity: sha512-Nc3loyVASW59W+8fLDZT1lncpG7llffyZ2o0UQLx/Fr20i7P8oP+lE7+TEcFvXj9IUWU6LjB9P3BH+iFGyp+mg==} engines: {node: ^14.16.0 || >=16.0.0} - next@14.2.30: - resolution: {integrity: sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - sass: - optional: true - next@14.2.31: resolution: {integrity: sha512-Wyw1m4t8PhqG+or5a1U/Deb888YApC4rAez9bGhHkTsfwAy4SWKVro0GhEx4sox1856IbLhvhce2hAA6o8vkog==} engines: {node: '>=18.17.0'} @@ -18121,61 +18046,32 @@ snapshots: - rollup - supports-color - '@next/env@14.2.30': {} - '@next/env@14.2.31': {} - '@next/swc-darwin-arm64@14.2.30': - optional: true - '@next/swc-darwin-arm64@14.2.31': optional: true - '@next/swc-darwin-x64@14.2.30': - optional: true - '@next/swc-darwin-x64@14.2.31': optional: true - '@next/swc-linux-arm64-gnu@14.2.30': - optional: true - '@next/swc-linux-arm64-gnu@14.2.31': optional: true - '@next/swc-linux-arm64-musl@14.2.30': - optional: true - '@next/swc-linux-arm64-musl@14.2.31': optional: true - '@next/swc-linux-x64-gnu@14.2.30': - optional: true - '@next/swc-linux-x64-gnu@14.2.31': optional: true - '@next/swc-linux-x64-musl@14.2.30': - optional: true - '@next/swc-linux-x64-musl@14.2.31': optional: true - '@next/swc-win32-arm64-msvc@14.2.30': - optional: true - '@next/swc-win32-arm64-msvc@14.2.31': optional: true - '@next/swc-win32-ia32-msvc@14.2.30': - optional: true - '@next/swc-win32-ia32-msvc@14.2.31': optional: true - '@next/swc-win32-x64-msvc@14.2.30': - optional: true - '@next/swc-win32-x64-msvc@14.2.31': optional: true @@ -27682,33 +27578,6 @@ snapshots: p-wait-for: 5.0.2 qs: 6.14.0 - next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@next/env': 14.2.30 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001723 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.28.0)(babel-plugin-macros@3.1.0)(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 14.2.30 - '@next/swc-darwin-x64': 14.2.30 - '@next/swc-linux-arm64-gnu': 14.2.30 - '@next/swc-linux-arm64-musl': 14.2.30 - '@next/swc-linux-x64-gnu': 14.2.30 - '@next/swc-linux-x64-musl': 14.2.30 - '@next/swc-win32-arm64-msvc': 14.2.30 - '@next/swc-win32-ia32-msvc': 14.2.30 - '@next/swc-win32-x64-msvc': 14.2.30 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.54.1 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@14.2.31(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.31 From 239b6baee8ca8d95163b0d7657f02529da3b6cd7 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 12:52:20 -0700 Subject: [PATCH 36/51] chore: add secret key --- packages/backend/src/api/resources/JSON.ts | 1 + packages/backend/src/api/resources/Machine.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 87bc0d1a379..21407ad79f6 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -714,6 +714,7 @@ export interface MachineJSON extends ClerkResourceJSON { updated_at: number; default_token_ttl: number; scoped_machines: MachineJSON[]; + secret_key?: string; } export interface MachineScopeJSON { diff --git a/packages/backend/src/api/resources/Machine.ts b/packages/backend/src/api/resources/Machine.ts index 8a096e35276..079ca8a2e7f 100644 --- a/packages/backend/src/api/resources/Machine.ts +++ b/packages/backend/src/api/resources/Machine.ts @@ -9,6 +9,7 @@ export class Machine { readonly updatedAt: number, readonly scopedMachines: Machine[], readonly defaultTokenTtl: number, + readonly secretKey?: string, ) {} static fromJSON(data: MachineJSON): Machine { @@ -31,6 +32,7 @@ export class Machine { ), ), data.default_token_ttl, + data.secret_key, ); } } From a8d310fcc6c7989156e215ea877e670e28dba8ef Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 13:24:04 -0700 Subject: [PATCH 37/51] chore: do not destructure body params in m2m endpoints --- .../backend/src/api/endpoints/MachineTokenApi.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineTokenApi.ts b/packages/backend/src/api/endpoints/MachineTokenApi.ts index e1822819329..d884e695dbf 100644 --- a/packages/backend/src/api/endpoints/MachineTokenApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokenApi.ts @@ -8,6 +8,7 @@ const basePath = '/m2m_tokens'; type CreateMachineTokenParams = { machineSecret: string; secondsUntilExpiration?: number | null; + claims?: Record | null; }; type RevokeMachineTokenParams = { @@ -29,14 +30,17 @@ export class MachineTokenApi extends AbstractAPI { } async create(params: CreateMachineTokenParams) { - const { machineSecret, ...bodyParams } = params; + const { machineSecret, claims = null, secondsUntilExpiration = null } = params; this.#requireMachineSecret(machineSecret); return this.request({ method: 'POST', path: basePath, - bodyParams, + bodyParams: { + secondsUntilExpiration, + claims, + }, options: { skipSecretKeyAuthorization: true, }, @@ -47,14 +51,16 @@ export class MachineTokenApi extends AbstractAPI { } async revoke(params: RevokeMachineTokenParams) { - const { m2mTokenId, machineSecret, ...bodyParams } = params; + const { m2mTokenId, machineSecret, revocationReason = null } = params; this.requireId(m2mTokenId); const requestOptions: ClerkBackendApiRequestOptions = { method: 'POST', path: joinPaths(basePath, m2mTokenId, 'revoke'), - bodyParams, + bodyParams: { + revocationReason, + }, }; if (machineSecret) { From 2a74cf9ce18e52cd9ab2c6b54f408c4709bdb67b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 13:25:48 -0700 Subject: [PATCH 38/51] chore: do not destructure body params in machine endpoints --- packages/backend/src/api/endpoints/MachineApi.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/api/endpoints/MachineApi.ts b/packages/backend/src/api/endpoints/MachineApi.ts index 74e51ad9204..cb695bb2a41 100644 --- a/packages/backend/src/api/endpoints/MachineApi.ts +++ b/packages/backend/src/api/endpoints/MachineApi.ts @@ -69,12 +69,15 @@ export class MachineApi extends AbstractAPI { } async update(params: UpdateMachineParams) { - const { machineId, ...bodyParams } = params; + const { machineId, name = null, defaultTokenTtl = null } = params; this.requireId(machineId); return this.request({ method: 'PATCH', path: joinPaths(basePath, machineId), - bodyParams, + bodyParams: { + name, + defaultTokenTtl, + }, }); } From 60b139ada9068989bab8049f3e159564df9ad1fe Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 13:32:19 -0700 Subject: [PATCH 39/51] chore: update tests --- packages/backend/src/api/__tests__/MachineApi.test.ts | 7 +++++-- packages/backend/src/api/endpoints/MachineApi.ts | 7 ++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineApi.test.ts b/packages/backend/src/api/__tests__/MachineApi.test.ts index 9b721206211..25e8594fb62 100644 --- a/packages/backend/src/api/__tests__/MachineApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineApi.test.ts @@ -127,7 +127,10 @@ describe('MachineAPI', () => { validateHeaders(async ({ request }) => { const body = await request.json(); expect(body).toEqual({ name: 'Updated Machine' }); - return HttpResponse.json(mockMachine); + return HttpResponse.json({ + ...mockMachine, + name: 'Updated Machine', + }); }), ), ); @@ -135,7 +138,7 @@ describe('MachineAPI', () => { const response = await apiClient.machines.update(updateParams); expect(response.id).toBe(machineId); - expect(response.name).toBe('Test Machine'); + expect(response.name).toBe('Updated Machine'); }); it('deletes a machine', async () => { diff --git a/packages/backend/src/api/endpoints/MachineApi.ts b/packages/backend/src/api/endpoints/MachineApi.ts index cb695bb2a41..74e51ad9204 100644 --- a/packages/backend/src/api/endpoints/MachineApi.ts +++ b/packages/backend/src/api/endpoints/MachineApi.ts @@ -69,15 +69,12 @@ export class MachineApi extends AbstractAPI { } async update(params: UpdateMachineParams) { - const { machineId, name = null, defaultTokenTtl = null } = params; + const { machineId, ...bodyParams } = params; this.requireId(machineId); return this.request({ method: 'PATCH', path: joinPaths(basePath, machineId), - bodyParams: { - name, - defaultTokenTtl, - }, + bodyParams, }); } From ad9a0ecbc5616a4529e95910123a93fb142ac3d3 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 15:36:19 -0700 Subject: [PATCH 40/51] chore: Use machine secret key from created Clerk client --- .changeset/hot-tables-worry.md | 17 ++-- .../src/api/__tests__/MachineTokenApi.test.ts | 77 ++++++++----------- .../src/api/endpoints/MachineTokenApi.ts | 55 +++---------- packages/backend/src/api/factory.ts | 1 + packages/backend/src/api/request.ts | 33 ++++++-- packages/backend/src/index.ts | 2 +- .../src/tokens/__tests__/request.test.ts | 6 +- .../src/tokens/__tests__/verify.test.ts | 4 +- packages/backend/src/tokens/request.ts | 2 +- packages/backend/src/tokens/verify.ts | 7 +- .../backend/src/util/optionsAssertions.ts | 6 ++ 11 files changed, 93 insertions(+), 117 deletions(-) diff --git a/.changeset/hot-tables-worry.md b/.changeset/hot-tables-worry.md index 4dc86485d8e..bbeefa22f31 100644 --- a/.changeset/hot-tables-worry.md +++ b/.changeset/hot-tables-worry.md @@ -9,10 +9,12 @@ Adds machine-to-machine endpoints to the Backend SDK: A machine secret is required when creating M2M tokens. ```ts -const clerkClient = createClerkClient() +const clerkClient = createClerkClient({ + machineSecretKey: process.env.CLERK_MACHINE_SECRET_KEY +}) clerkClient.machineTokens.create({ - machineSecret: 'ak_xxxxx', + secondsUntilExpiration: 3600, }) ``` @@ -22,9 +24,8 @@ You can revoke tokens using either a machine secret or instance secret: ```ts // Using machine secret -const clerkClient = createClerkClient() +const clerkClient = createClerkClient({ machineSecretKey: process.env.CLERK_MACHINE_SECRET_KEY }) clerkClient.machineTokens.revoke({ - machineSecret: 'ak_xxxxx', m2mTokenId: 'mt_xxxxx', revocationReason: 'Revoked by user', }) @@ -43,9 +44,8 @@ You can verify tokens using either a machine secret or instance secret: ```ts // Using machine secret -const clerkClient = createClerkClient() +const clerkClient = createClerkClient({ machineSecretKey: process.env.CLERK_MACHINE_SECRET_KEY }) clerkClient.machineTokens.verifySecret({ - machineSecret: 'ak_xxxxx', secret: 'mt_secret_xxxxx', }) @@ -59,11 +59,12 @@ clerkClient.machineTokens.verifySecret({ To verify machine-to-machine tokens using when using `authenticateRequest()` with a machine secret, use the `machineSecret` option: ```ts -const clerkClient = createClerkClient() +const clerkClient = createClerkClient({ + machineSecretKey: process.env.CLERK_MACHINE_SECRET_KEY +}) const authReq = await clerkClient.authenticateRequest(c.req.raw, { acceptsToken: 'machine_token', - machineSecret: 'ak_xxxxx' }) if (authReq.isAuthenticated) { diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index d3f2c5225d5..d87de0dd008 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -27,13 +27,9 @@ describe('MachineTokenAPI', () => { it('creates a m2m token using machine secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', + machineSecretKey: 'ak_xxxxx', }); - const createParams = { - machineSecret: 'ak_xxxxx', - secondsUntilExpiration: 3600, - }; - server.use( http.post( 'https://api.clerk.test/m2m_tokens', @@ -44,7 +40,9 @@ describe('MachineTokenAPI', () => { ), ); - const response = await apiClient.machineTokens.create(createParams); + const response = await apiClient.machineTokens.create({ + secondsUntilExpiration: 3600, + }); expect(response.id).toBe(m2mId); expect(response.secret).toBe(m2mSecret); @@ -67,10 +65,9 @@ describe('MachineTokenAPI', () => { ), ); - // @ts-expect-error - machineSecret is required - const response = await apiClient.machineTokens.create({}).catch(err => err); + const response = await apiClient.machineTokens.create().catch(err => err); - expect(response.message).toBe('Missing machine secret.'); + expect(response.message).toBe('Missing Clerk Machine Secret Key.'); }); }); @@ -92,14 +89,9 @@ describe('MachineTokenAPI', () => { it('revokes a m2m token using machine secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', + machineSecretKey: 'ak_xxxxx', }); - const revokeParams = { - machineSecret: 'ak_xxxxx', - m2mTokenId: m2mId, - revocationReason: 'revoked by test', - }; - server.use( http.post( `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, @@ -110,7 +102,10 @@ describe('MachineTokenAPI', () => { ), ); - const response = await apiClient.machineTokens.revoke(revokeParams); + const response = await apiClient.machineTokens.revoke({ + m2mTokenId: m2mId, + revocationReason: 'revoked by test', + }); expect(response.revoked).toBe(true); expect(response.secret).toBeUndefined(); @@ -125,11 +120,6 @@ describe('MachineTokenAPI', () => { secretKey: 'sk_xxxxx', }); - const revokeParams = { - m2mTokenId: m2mId, - revocationReason: 'revoked by test', - }; - server.use( http.post( `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, @@ -140,7 +130,10 @@ describe('MachineTokenAPI', () => { ), ); - const response = await apiClient.machineTokens.revoke(revokeParams); + const response = await apiClient.machineTokens.revoke({ + m2mTokenId: m2mId, + revocationReason: 'revoked using instance secret', + }); expect(response.revoked).toBe(true); expect(response.secret).toBeUndefined(); @@ -152,11 +145,6 @@ describe('MachineTokenAPI', () => { apiUrl: 'https://api.clerk.test', }); - const revokeParams = { - m2mTokenId: m2mId, - revocationReason: 'revoked by test', - }; - server.use( http.post( `https://api.clerk.test/m2m_tokens/${m2mId}/revoke`, @@ -166,7 +154,12 @@ describe('MachineTokenAPI', () => { ), ); - const errResponse = await apiClient.machineTokens.revoke(revokeParams).catch(err => err); + const errResponse = await apiClient.machineTokens + .revoke({ + m2mTokenId: m2mId, + revocationReason: 'revoked by test', + }) + .catch(err => err); expect(errResponse.status).toBe(401); }); @@ -176,13 +169,9 @@ describe('MachineTokenAPI', () => { it('verifies a m2m token using machine secret', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', + machineSecretKey: 'ak_xxxxx', }); - const verifyParams = { - machineSecret: 'ak_xxxxx', - secret: m2mSecret, - }; - server.use( http.post( 'https://api.clerk.test/m2m_tokens/verify', @@ -193,7 +182,9 @@ describe('MachineTokenAPI', () => { ), ); - const response = await apiClient.machineTokens.verifySecret(verifyParams); + const response = await apiClient.machineTokens.verifySecret({ + secret: m2mSecret, + }); expect(response.id).toBe(m2mId); expect(response.secret).toBe(m2mSecret); @@ -207,10 +198,6 @@ describe('MachineTokenAPI', () => { secretKey: 'sk_xxxxx', }); - const verifyParams = { - secret: m2mSecret, - }; - server.use( http.post( 'https://api.clerk.test/m2m_tokens/verify', @@ -221,7 +208,9 @@ describe('MachineTokenAPI', () => { ), ); - const response = await apiClient.machineTokens.verifySecret(verifyParams); + const response = await apiClient.machineTokens.verifySecret({ + secret: m2mSecret, + }); expect(response.id).toBe(m2mId); expect(response.secret).toBe(m2mSecret); @@ -234,10 +223,6 @@ describe('MachineTokenAPI', () => { apiUrl: 'https://api.clerk.test', }); - const verifyParams = { - secret: m2mSecret, - }; - server.use( http.post( 'https://api.clerk.test/m2m_tokens/verify', @@ -247,7 +232,11 @@ describe('MachineTokenAPI', () => { ), ); - const errResponse = await apiClient.machineTokens.verifySecret(verifyParams).catch(err => err); + const errResponse = await apiClient.machineTokens + .verifySecret({ + secret: m2mSecret, + }) + .catch(err => err); expect(errResponse.status).toBe(401); }); diff --git a/packages/backend/src/api/endpoints/MachineTokenApi.ts b/packages/backend/src/api/endpoints/MachineTokenApi.ts index d884e695dbf..ea3d8ceba7c 100644 --- a/packages/backend/src/api/endpoints/MachineTokenApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokenApi.ts @@ -1,38 +1,26 @@ import { joinPaths } from '../../util/path'; -import type { ClerkBackendApiRequestOptions } from '../request'; import type { MachineToken } from '../resources/MachineToken'; import { AbstractAPI } from './AbstractApi'; const basePath = '/m2m_tokens'; type CreateMachineTokenParams = { - machineSecret: string; secondsUntilExpiration?: number | null; claims?: Record | null; }; type RevokeMachineTokenParams = { - machineSecret?: string | null; m2mTokenId: string; revocationReason?: string | null; }; type VerifyMachineTokenParams = { - machineSecret?: string | null; secret: string; }; export class MachineTokenApi extends AbstractAPI { - #requireMachineSecret(machineSecret?: string | null): asserts machineSecret is string { - if (!machineSecret) { - throw new Error('Missing machine secret.'); - } - } - - async create(params: CreateMachineTokenParams) { - const { machineSecret, claims = null, secondsUntilExpiration = null } = params; - - this.#requireMachineSecret(machineSecret); + async create(params?: CreateMachineTokenParams) { + const { claims = null, secondsUntilExpiration = null } = params || {}; return this.request({ method: 'POST', @@ -42,57 +30,32 @@ export class MachineTokenApi extends AbstractAPI { claims, }, options: { - skipSecretKeyAuthorization: true, - }, - headerParams: { - Authorization: `Bearer ${machineSecret}`, + requireMachineSecretKey: true, }, }); } async revoke(params: RevokeMachineTokenParams) { - const { m2mTokenId, machineSecret, revocationReason = null } = params; + const { m2mTokenId, revocationReason = null } = params; this.requireId(m2mTokenId); - const requestOptions: ClerkBackendApiRequestOptions = { + return this.request({ method: 'POST', path: joinPaths(basePath, m2mTokenId, 'revoke'), bodyParams: { revocationReason, }, - }; - - if (machineSecret) { - requestOptions.options = { - skipSecretKeyAuthorization: true, - }; - requestOptions.headerParams = { - Authorization: `Bearer ${machineSecret}`, - }; - } - - return this.request(requestOptions); + }); } async verifySecret(params: VerifyMachineTokenParams) { - const { machineSecret, secret } = params; + const { secret } = params; - const requestOptions: ClerkBackendApiRequestOptions = { + return this.request({ method: 'POST', path: joinPaths(basePath, 'verify'), bodyParams: { secret }, - }; - - if (machineSecret) { - requestOptions.options = { - skipSecretKeyAuthorization: true, - }; - requestOptions.headerParams = { - Authorization: `Bearer ${machineSecret}`, - }; - } - - return this.request(requestOptions); + }); } } diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index 6af1dba5581..b3336e952dd 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -71,6 +71,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { ...options, skipApiVersionInUrl: true, requireSecretKey: false, + useMachineSecretKey: true, }), ), oauthApplications: new OAuthApplicationsApi(request), diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index bbe8c9a3c97..da7bad8f37b 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -4,7 +4,7 @@ import snakecaseKeys from 'snakecase-keys'; import { API_URL, API_VERSION, constants, SUPPORTED_BAPI_VERSION, USER_AGENT } from '../constants'; import { runtime } from '../runtime'; -import { assertValidSecretKey } from '../util/optionsAssertions'; +import { assertValidMachineSecretKey, assertValidSecretKey } from '../util/optionsAssertions'; import { joinPaths } from '../util/path'; import { deserialize } from './resources/Deserializer'; @@ -28,18 +28,17 @@ type ClerkBackendApiRequestOptionsBodyParams = */ deepSnakecaseBodyParamKeys?: boolean; /** - * If true, skips adding the instance secret key to the authorization header. - * Useful when you need to provide custom authorization (e.g., machine secrets). + * If true, requires a machine secret key to be provided. * @default false */ - skipSecretKeyAuthorization?: boolean; + requireMachineSecretKey?: boolean; }; } | { bodyParams?: never; options?: { deepSnakecaseBodyParamKeys?: boolean; - skipSecretKeyAuthorization?: never; + requireMachineSecretKey?: boolean; }; }; @@ -92,12 +91,24 @@ type BuildRequestOptions = { * @default false */ skipApiVersionInUrl?: boolean; + /* Machine secret key */ + machineSecretKey?: string; + /** + * If true, uses machineSecretKey for authorization instead of secretKey. + * + * Note: This is only used for machine-to-machine tokens. + * + * @default false + */ + useMachineSecretKey?: boolean; }; export function buildRequest(options: BuildRequestOptions) { const requestFn = async (requestOptions: ClerkBackendApiRequestOptions): Promise> => { const { secretKey, + machineSecretKey, + useMachineSecretKey = false, requireSecretKey = true, apiUrl = API_URL, apiVersion = API_VERSION, @@ -105,12 +116,16 @@ export function buildRequest(options: BuildRequestOptions) { skipApiVersionInUrl = false, } = options; const { path, method, queryParams, headerParams, bodyParams, formData, options: opts } = requestOptions; - const { deepSnakecaseBodyParamKeys = false, skipSecretKeyAuthorization = false } = opts || {}; + const { deepSnakecaseBodyParamKeys = false, requireMachineSecretKey = false } = opts || {}; if (requireSecretKey) { assertValidSecretKey(secretKey); } + if (requireMachineSecretKey) { + assertValidMachineSecretKey(machineSecretKey); + } + const url = skipApiVersionInUrl ? joinPaths(apiUrl, path) : joinPaths(apiUrl, apiVersion, path); // Build final URL with search parameters @@ -135,7 +150,11 @@ export function buildRequest(options: BuildRequestOptions) { ...headerParams, }); - if (secretKey && !skipSecretKeyAuthorization) { + // When useMachineSecretKey is true and machineSecretKey is provided, use machine auth + // Otherwise, fall back to regular secretKey auth for all existing APIs + if (useMachineSecretKey && machineSecretKey) { + headers.set(constants.Headers.Authorization, `Bearer ${machineSecretKey}`); + } else if (secretKey) { headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`); } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 6f4e3aee673..7d072d4e841 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -11,7 +11,7 @@ import { verifyToken as _verifyToken } from './tokens/verify'; export const verifyToken = withLegacyReturn(_verifyToken); -export type ClerkOptions = CreateBackendApiOptions & +export type ClerkOptions = Omit & Partial< Pick< CreateAuthenticateRequestOptions['options'], diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index 7237e6338fe..3f587d52ee5 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -1253,20 +1253,20 @@ describe('tokens.authenticateRequest(options)', () => { const request = mockRequest({ authorization: `Bearer ${mockTokens.machine_token}` }); const requestState = await authenticateRequest( request, - mockOptions({ acceptsToken: 'machine_token', machineSecret: 'ak_xxxxx' }), + mockOptions({ acceptsToken: 'machine_token', machineSecretKey: 'ak_xxxxx' }), ); expect(requestState).toBeMachineAuthenticated(); }); - test('throws an error if acceptsToken is machine_token but machineSecret or secretKey is not provided', async () => { + test('throws an error if acceptsToken is machine_token but machineSecretKey or secretKey is not provided', async () => { const request = mockRequest({ authorization: `Bearer ${mockTokens.machine_token}` }); await expect( authenticateRequest(request, mockOptions({ acceptsToken: 'machine_token', secretKey: undefined })), ).rejects.toThrow( 'Machine token authentication requires either a machine secret or a Clerk secret key. ' + - 'Provide either the `machineSecret` option or ensure Clerk secret key is set.', + 'Provide either the `machineSecretKey` option or ensure Clerk secret key is set.', ); }); diff --git a/packages/backend/src/tokens/__tests__/verify.test.ts b/packages/backend/src/tokens/__tests__/verify.test.ts index a844f69b638..a943486e25e 100644 --- a/packages/backend/src/tokens/__tests__/verify.test.ts +++ b/packages/backend/src/tokens/__tests__/verify.test.ts @@ -140,8 +140,8 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { const result = await verifyMachineAuthToken(token, { apiUrl: 'https://api.clerk.test', - // @ts-expect-error - machineSecret from options - machineSecret: 'ak_xxxxx', + // @ts-expect-error: Machine secret key is only visible in createClerkClient() + machineSecretKey: 'ak_xxxxx', }); expect(result.tokenType).toBe('machine_token'); diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 9640ad5806f..c0a4f058ab8 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -64,7 +64,7 @@ function assertMachineSecretOrSecretKey(authenticateContext: AuthenticateContext if (!authenticateContext.machineSecret && !authenticateContext.secretKey) { throw new Error( 'Machine token authentication requires either a machine secret or a Clerk secret key. ' + - 'Provide either the `machineSecret` option or ensure Clerk secret key is set.', + 'Provide either the `machineSecretKey` option or ensure Clerk secret key is set.', ); } } diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index 3533896a5a2..6b3fac666fc 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -202,14 +202,11 @@ function handleClerkAPIError( async function verifyMachineToken( secret: string, - options: VerifyTokenOptions & { machineSecret?: string }, + options: VerifyTokenOptions & { machineSecretKey?: string }, ): Promise> { try { const client = createBackendApiClient(options); - const verifiedToken = await client.machineTokens.verifySecret({ - secret, - machineSecret: options.machineSecret, - }); + const verifiedToken = await client.machineTokens.verifySecret({ secret }); return { data: verifiedToken, tokenType: TokenType.MachineToken, errors: undefined }; } catch (err: any) { return handleClerkAPIError(TokenType.MachineToken, err, 'Machine token not found'); diff --git a/packages/backend/src/util/optionsAssertions.ts b/packages/backend/src/util/optionsAssertions.ts index 25be5f1d953..59eac3dbb19 100644 --- a/packages/backend/src/util/optionsAssertions.ts +++ b/packages/backend/src/util/optionsAssertions.ts @@ -11,3 +11,9 @@ export function assertValidSecretKey(val: unknown): asserts val is string { export function assertValidPublishableKey(val: unknown): asserts val is string { parsePublishableKey(val as string | undefined, { fatal: true }); } + +export function assertValidMachineSecretKey(val: unknown): asserts val is string { + if (!val || typeof val !== 'string') { + throw Error('Missing Clerk Machine Secret Key.'); + } +} From 8ca8009f84793a666f93ad8cbd8f53fc6677a5d4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 15:49:03 -0700 Subject: [PATCH 41/51] chore: update missing clerk instance key or machine secret key error --- packages/backend/src/tokens/__tests__/request.test.ts | 4 ++-- packages/backend/src/tokens/request.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index 3f587d52ee5..a0eb8f1b40e 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -1265,8 +1265,8 @@ describe('tokens.authenticateRequest(options)', () => { await expect( authenticateRequest(request, mockOptions({ acceptsToken: 'machine_token', secretKey: undefined })), ).rejects.toThrow( - 'Machine token authentication requires either a machine secret or a Clerk secret key. ' + - 'Provide either the `machineSecretKey` option or ensure Clerk secret key is set.', + 'Machine token authentication requires either a Machine secret key or a Clerk secret key. ' + + 'Ensure a Clerk secret key or Machine secret key is set.', ); }); diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index c0a4f058ab8..068e0f841d4 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -63,8 +63,8 @@ function assertSignInUrlFormatAndOrigin(_signInUrl: string, origin: string) { function assertMachineSecretOrSecretKey(authenticateContext: AuthenticateContext) { if (!authenticateContext.machineSecret && !authenticateContext.secretKey) { throw new Error( - 'Machine token authentication requires either a machine secret or a Clerk secret key. ' + - 'Provide either the `machineSecretKey` option or ensure Clerk secret key is set.', + 'Machine token authentication requires either a Machine secret key or a Clerk secret key. ' + + 'Ensure a Clerk secret key or Machine secret key is set.', ); } } From 7569d958863e487c431e76e78dc1f2e71780c391 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 15:49:13 -0700 Subject: [PATCH 42/51] formatting --- packages/backend/src/tokens/request.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 068e0f841d4..552813bee4c 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -68,6 +68,7 @@ function assertMachineSecretOrSecretKey(authenticateContext: AuthenticateContext ); } } + function isRequestEligibleForRefresh( err: TokenVerificationError, authenticateContext: { refreshTokenInCookie?: string }, From ad5a0f97c605c18f34410ce1aece93e6b37e4ff2 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 16:01:19 -0700 Subject: [PATCH 43/51] fix authenticate request option types --- packages/backend/src/tokens/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts index 72534c4fe05..d7a62eb2271 100644 --- a/packages/backend/src/tokens/types.ts +++ b/packages/backend/src/tokens/types.ts @@ -72,7 +72,7 @@ export type AuthenticateRequestOptions = { * This will override the Clerk secret key. * @internal */ - machineSecret?: string; + machineSecretKey?: string; } & VerifyTokenOptions; /** From b72e8915ddd84e8e8766eac7e56f56d1feacf80b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 16:10:41 -0700 Subject: [PATCH 44/51] fix assertion --- packages/backend/src/tokens/request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 552813bee4c..e3f87ee5925 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -61,7 +61,7 @@ function assertSignInUrlFormatAndOrigin(_signInUrl: string, origin: string) { } function assertMachineSecretOrSecretKey(authenticateContext: AuthenticateContext) { - if (!authenticateContext.machineSecret && !authenticateContext.secretKey) { + if (!authenticateContext.machineSecretKey && !authenticateContext.secretKey) { throw new Error( 'Machine token authentication requires either a Machine secret key or a Clerk secret key. ' + 'Ensure a Clerk secret key or Machine secret key is set.', From 2b2ce8ec07fd2754be05d52fc3da20766bb2008b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 16:17:31 -0700 Subject: [PATCH 45/51] Add machine secret key to merged options --- packages/backend/src/tokens/factory.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/tokens/factory.ts b/packages/backend/src/tokens/factory.ts index 0bc9cf3c4ae..7f0c3916608 100644 --- a/packages/backend/src/tokens/factory.ts +++ b/packages/backend/src/tokens/factory.ts @@ -17,11 +17,13 @@ type BuildTimeOptions = Partial< | 'proxyUrl' | 'publishableKey' | 'secretKey' + | 'machineSecretKey' > >; const defaultOptions = { secretKey: '', + machineSecretKey: '', jwtKey: '', apiUrl: undefined, apiVersion: undefined, From afb592343ce2fceebc90e09c97f517bc0f62af29 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 20:05:05 -0700 Subject: [PATCH 46/51] allow custom machine secret key per method --- .../src/api/__tests__/MachineTokenApi.test.ts | 21 ++++- .../src/api/endpoints/MachineTokenApi.ts | 79 +++++++++++++------ packages/backend/src/api/request.ts | 28 +++---- .../backend/src/util/optionsAssertions.ts | 6 -- 4 files changed, 82 insertions(+), 52 deletions(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index d87de0dd008..9dfc3d38620 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -60,14 +60,29 @@ describe('MachineTokenAPI', () => { http.post( 'https://api.clerk.test/m2m_tokens', validateHeaders(() => { - return HttpResponse.json(mockM2MToken); + return HttpResponse.json( + { + errors: [ + { + message: + 'The provided Machine Secret Key is invalid. Make sure that your Machine Secret Key is correct.', + code: 'machine_secret_key_invalid', + }, + ], + }, + { status: 401 }, + ); }), ), ); - const response = await apiClient.machineTokens.create().catch(err => err); + const errResponse = await apiClient.machineTokens.create().catch(err => err); - expect(response.message).toBe('Missing Clerk Machine Secret Key.'); + expect(errResponse.status).toBe(401); + expect(errResponse.errors[0].code).toBe('machine_secret_key_invalid'); + expect(errResponse.errors[0].message).toBe( + 'The provided Machine Secret Key is invalid. Make sure that your Machine Secret Key is correct.', + ); }); }); diff --git a/packages/backend/src/api/endpoints/MachineTokenApi.ts b/packages/backend/src/api/endpoints/MachineTokenApi.ts index ea3d8ceba7c..a9622c504e2 100644 --- a/packages/backend/src/api/endpoints/MachineTokenApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokenApi.ts @@ -1,3 +1,4 @@ +import type { ClerkBackendApiRequestOptions } from '../../api/request'; import { joinPaths } from '../../util/path'; import type { MachineToken } from '../resources/MachineToken'; import { AbstractAPI } from './AbstractApi'; @@ -5,57 +6,85 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/m2m_tokens'; type CreateMachineTokenParams = { + machineSecretKey?: string; secondsUntilExpiration?: number | null; claims?: Record | null; }; type RevokeMachineTokenParams = { + machineSecretKey?: string; m2mTokenId: string; revocationReason?: string | null; }; type VerifyMachineTokenParams = { + machineSecretKey?: string; secret: string; }; export class MachineTokenApi extends AbstractAPI { + #createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) { + if (machineSecretKey) { + return { + ...options, + headerParams: { + Authorization: `Bearer ${machineSecretKey}`, + }, + }; + } + + return options; + } + async create(params?: CreateMachineTokenParams) { - const { claims = null, secondsUntilExpiration = null } = params || {}; - - return this.request({ - method: 'POST', - path: basePath, - bodyParams: { - secondsUntilExpiration, - claims, - }, - options: { - requireMachineSecretKey: true, + const { claims = null, machineSecretKey, secondsUntilExpiration = null } = params || {}; + + const requestOptions = this.#createRequestOptions( + { + method: 'POST', + path: basePath, + bodyParams: { + secondsUntilExpiration, + claims, + }, }, - }); + machineSecretKey, + ); + + return this.request(requestOptions); } async revoke(params: RevokeMachineTokenParams) { - const { m2mTokenId, revocationReason = null } = params; + const { m2mTokenId, revocationReason = null, machineSecretKey } = params; this.requireId(m2mTokenId); - return this.request({ - method: 'POST', - path: joinPaths(basePath, m2mTokenId, 'revoke'), - bodyParams: { - revocationReason, + const requestOptions = this.#createRequestOptions( + { + method: 'POST', + path: joinPaths(basePath, m2mTokenId, 'revoke'), + bodyParams: { + revocationReason, + }, }, - }); + machineSecretKey, + ); + + return this.request(requestOptions); } async verifySecret(params: VerifyMachineTokenParams) { - const { secret } = params; + const { secret, machineSecretKey } = params; + + const requestOptions = this.#createRequestOptions( + { + method: 'POST', + path: joinPaths(basePath, 'verify'), + bodyParams: { secret }, + }, + machineSecretKey, + ); - return this.request({ - method: 'POST', - path: joinPaths(basePath, 'verify'), - bodyParams: { secret }, - }); + return this.request(requestOptions); } } diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index da7bad8f37b..3e99fac4c36 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -4,7 +4,7 @@ import snakecaseKeys from 'snakecase-keys'; import { API_URL, API_VERSION, constants, SUPPORTED_BAPI_VERSION, USER_AGENT } from '../constants'; import { runtime } from '../runtime'; -import { assertValidMachineSecretKey, assertValidSecretKey } from '../util/optionsAssertions'; +import { assertValidSecretKey } from '../util/optionsAssertions'; import { joinPaths } from '../util/path'; import { deserialize } from './resources/Deserializer'; @@ -27,18 +27,12 @@ type ClerkBackendApiRequestOptionsBodyParams = * @default false */ deepSnakecaseBodyParamKeys?: boolean; - /** - * If true, requires a machine secret key to be provided. - * @default false - */ - requireMachineSecretKey?: boolean; }; } | { bodyParams?: never; options?: { deepSnakecaseBodyParamKeys?: boolean; - requireMachineSecretKey?: boolean; }; }; @@ -116,16 +110,12 @@ export function buildRequest(options: BuildRequestOptions) { skipApiVersionInUrl = false, } = options; const { path, method, queryParams, headerParams, bodyParams, formData, options: opts } = requestOptions; - const { deepSnakecaseBodyParamKeys = false, requireMachineSecretKey = false } = opts || {}; + const { deepSnakecaseBodyParamKeys = false } = opts || {}; if (requireSecretKey) { assertValidSecretKey(secretKey); } - if (requireMachineSecretKey) { - assertValidMachineSecretKey(machineSecretKey); - } - const url = skipApiVersionInUrl ? joinPaths(apiUrl, path) : joinPaths(apiUrl, apiVersion, path); // Build final URL with search parameters @@ -150,12 +140,14 @@ export function buildRequest(options: BuildRequestOptions) { ...headerParams, }); - // When useMachineSecretKey is true and machineSecretKey is provided, use machine auth - // Otherwise, fall back to regular secretKey auth for all existing APIs - if (useMachineSecretKey && machineSecretKey) { - headers.set(constants.Headers.Authorization, `Bearer ${machineSecretKey}`); - } else if (secretKey) { - headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`); + // If Authorization header already exists, preserve it. + // Otherwise, use machine secret key if enabled, or fall back to regular secret key + if (!headers.has(constants.Headers.Authorization)) { + if (useMachineSecretKey && machineSecretKey) { + headers.set(constants.Headers.Authorization, `Bearer ${machineSecretKey}`); + } else if (secretKey) { + headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`); + } } let res: Response | undefined; diff --git a/packages/backend/src/util/optionsAssertions.ts b/packages/backend/src/util/optionsAssertions.ts index 59eac3dbb19..25be5f1d953 100644 --- a/packages/backend/src/util/optionsAssertions.ts +++ b/packages/backend/src/util/optionsAssertions.ts @@ -11,9 +11,3 @@ export function assertValidSecretKey(val: unknown): asserts val is string { export function assertValidPublishableKey(val: unknown): asserts val is string { parsePublishableKey(val as string | undefined, { fatal: true }); } - -export function assertValidMachineSecretKey(val: unknown): asserts val is string { - if (!val || typeof val !== 'string') { - throw Error('Missing Clerk Machine Secret Key.'); - } -} From 01653ae1c3004f607cab5df629b0a1a5babc0a15 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 20:08:00 -0700 Subject: [PATCH 47/51] add jsdoc --- packages/backend/src/api/endpoints/MachineTokenApi.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/backend/src/api/endpoints/MachineTokenApi.ts b/packages/backend/src/api/endpoints/MachineTokenApi.ts index a9622c504e2..91bbe16d9aa 100644 --- a/packages/backend/src/api/endpoints/MachineTokenApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokenApi.ts @@ -6,18 +6,27 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/m2m_tokens'; type CreateMachineTokenParams = { + /** + * Custom machine secret key for authentication. + */ machineSecretKey?: string; secondsUntilExpiration?: number | null; claims?: Record | null; }; type RevokeMachineTokenParams = { + /** + * Custom machine secret key for authentication. + */ machineSecretKey?: string; m2mTokenId: string; revocationReason?: string | null; }; type VerifyMachineTokenParams = { + /** + * Custom machine secret key for authentication. + */ machineSecretKey?: string; secret: string; }; From e05d4145d60b6e0456dec485937f485c170e5397 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 4 Aug 2025 21:22:09 -0700 Subject: [PATCH 48/51] chore: separate backend api client machine secret key and options secret key test --- .../src/api/__tests__/MachineTokenApi.test.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts index 9dfc3d38620..7207f64c6cb 100644 --- a/packages/backend/src/api/__tests__/MachineTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/MachineTokenApi.test.ts @@ -24,7 +24,7 @@ describe('MachineTokenAPI', () => { }; describe('create', () => { - it('creates a m2m token using machine secret', async () => { + it('creates a m2m token using machine secret key in backend client', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', machineSecretKey: 'ak_xxxxx', @@ -50,6 +50,32 @@ describe('MachineTokenAPI', () => { expect(response.claims).toEqual({ foo: 'bar' }); }); + it('creates a m2m token using machine secret key option', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + server.use( + http.post( + 'https://api.clerk.test/m2m_tokens', + validateHeaders(({ request }) => { + expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx'); + return HttpResponse.json(mockM2MToken); + }), + ), + ); + + const response = await apiClient.machineTokens.create({ + machineSecretKey: 'ak_xxxxx', + secondsUntilExpiration: 3600, + }); + + expect(response.id).toBe(m2mId); + expect(response.secret).toBe(m2mSecret); + expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']); + expect(response.claims).toEqual({ foo: 'bar' }); + }); + it('does not accept an instance secret as authorization header', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', From 85e47f1c401ff4a8ba6c805fe9df8ed555146608 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 5 Aug 2025 10:24:15 -0700 Subject: [PATCH 49/51] chore: clean up authorizationHeader --- packages/backend/src/api/request.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index 3e99fac4c36..09c0991c349 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -142,11 +142,12 @@ export function buildRequest(options: BuildRequestOptions) { // If Authorization header already exists, preserve it. // Otherwise, use machine secret key if enabled, or fall back to regular secret key - if (!headers.has(constants.Headers.Authorization)) { + const authorizationHeader = constants.Headers.Authorization; + if (!headers.has(authorizationHeader)) { if (useMachineSecretKey && machineSecretKey) { - headers.set(constants.Headers.Authorization, `Bearer ${machineSecretKey}`); + headers.set(authorizationHeader, `Bearer ${machineSecretKey}`); } else if (secretKey) { - headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`); + headers.set(authorizationHeader, `Bearer ${secretKey}`); } } From e2ede5ff84fa436bc54e375c63ec79f238cb4dd9 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 5 Aug 2025 19:52:23 -0700 Subject: [PATCH 50/51] clean up authenticate context --- .../backend/src/tokens/authenticateContext.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index b6a37d942bc..54e5b45ed9b 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -67,17 +67,20 @@ class AuthenticateContext implements AuthenticateContext { private clerkRequest: ClerkRequest, options: AuthenticateRequestOptions, ) { - // Even though the options are assigned to this later in this function - // we set the publishableKey here because it is being used in cookies/headers/handshake-values - // as part of getMultipleAppsCookie. - // Machine tokens don't require publishable keys. - if (options.acceptsToken !== TokenType.MachineToken) { + if (options.acceptsToken === TokenType.MachineToken) { + // For machine tokens, we only need to set the header values. + this.initHeaderValues(); + } else { + // Even though the options are assigned to this later in this function + // we set the publishableKey here because it is being used in cookies/headers/handshake-values + // as part of getMultipleAppsCookie. this.initPublishableKeyValues(options); + this.initHeaderValues(); + // initCookieValues should be used before initHandshakeValues because it depends on suffixedCookies + this.initCookieValues(); + this.initHandshakeValues(); } - this.initHeaderValues(); - // initCookieValues should be used before initHandshakeValues because it depends on suffixedCookies - this.initCookieValues(); - this.initHandshakeValues(); + Object.assign(this, options); this.clerkUrl = this.clerkRequest.clerkUrl; } From c98d1ed07cac886295a99135961e0c47899c711f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 5 Aug 2025 19:57:39 -0700 Subject: [PATCH 51/51] clean up authenticate context --- packages/backend/src/tokens/authenticateContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index 54e5b45ed9b..e8385574866 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -67,8 +67,8 @@ class AuthenticateContext implements AuthenticateContext { private clerkRequest: ClerkRequest, options: AuthenticateRequestOptions, ) { - if (options.acceptsToken === TokenType.MachineToken) { - // For machine tokens, we only need to set the header values. + if (options.acceptsToken === TokenType.MachineToken || options.acceptsToken === TokenType.ApiKey) { + // For non-session tokens, we only want to set the header values. this.initHeaderValues(); } else { // Even though the options are assigned to this later in this function