Skip to content

feat(backend): Introduce M2M endpoints authentication using machine secret keys #6229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 61 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
05d6c8e
chore(backend): Introduce machine token secrets as authorization header
wobsoriano Jul 1, 2025
ca7a8be
chore: clean up
wobsoriano Jul 1, 2025
af6a27b
chore: use a more readable option for bapi proxy methods
wobsoriano Jul 1, 2025
fa94227
chore: add initial changeset
wobsoriano Jul 1, 2025
8dcd607
chore: add machine_secret_key type to api keys api
wobsoriano Jul 1, 2025
5d78030
Merge remote-tracking branch 'origin/main' into rob/user-2264-m2m
wobsoriano Jul 1, 2025
7bb3eb8
chore: reuse header consts
wobsoriano Jul 1, 2025
424a5a4
chore: rename to machine secret
wobsoriano Jul 1, 2025
1dbd41b
chore: clean up
wobsoriano Jul 1, 2025
7c3063c
chore: add secret property to create method
wobsoriano Jul 1, 2025
9dab708
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 1, 2025
db38ca5
chore: remove machine secret type from api key creation
wobsoriano Jul 1, 2025
5ce88ee
chore: make secret property optional
wobsoriano Jul 2, 2025
c33e3fd
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 2, 2025
f9526af
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 3, 2025
cb6c822
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 7, 2025
68bcb7e
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 8, 2025
e900e13
chore: add machines BAPI endpoints
wobsoriano Jul 8, 2025
6c0fc64
chore: trigger rebuild
wobsoriano Jul 8, 2025
c1d1ae2
chore: remove unnecessary params
wobsoriano Jul 8, 2025
d53115d
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 19, 2025
d87f937
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 25, 2025
7ff0538
chore: remove unused properties
wobsoriano Jul 25, 2025
e844565
chore: improve machine secret check
wobsoriano Jul 25, 2025
017bb4b
fix required secrets
wobsoriano Jul 25, 2025
e26660e
fix required secrets
wobsoriano Jul 25, 2025
0f7387d
fix required secrets
wobsoriano Jul 25, 2025
f78ddcc
chore: remove removed properties
wobsoriano Jul 25, 2025
1492a1e
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Jul 28, 2025
a8c66e1
chore: remove name and claims from m2m tokens
wobsoriano Jul 28, 2025
201cb23
fix tests
wobsoriano Jul 28, 2025
64afde6
fix incorrect method in tests
wobsoriano Jul 28, 2025
b38465b
chore: update tests
wobsoriano Jul 29, 2025
18b76da
chore: update test descriptions
wobsoriano Jul 29, 2025
d91404c
chore: improve tests
wobsoriano Jul 29, 2025
37a3d65
chore: update changeset
wobsoriano Jul 29, 2025
1d69db8
chore: skip pub key init for machine tokens
wobsoriano Jul 29, 2025
b26bd76
chore: skip pub and secret key check for authenticate request with ma…
wobsoriano Jul 29, 2025
d609285
fix error handling
wobsoriano Jul 29, 2025
2e080db
chore: allow machine secrets in authenticateRequest
wobsoriano Jul 29, 2025
7055b8a
chore: remove unused export keyword
wobsoriano Jul 29, 2025
051dd85
chore: more tests
wobsoriano Jul 29, 2025
7371a32
chore: add missing secret key or machine secret error test
wobsoriano Jul 29, 2025
d96d436
Merge branch 'main' into rob/user-2264-m2m
wobsoriano Aug 4, 2025
6642321
chore: run dedupe
wobsoriano Aug 4, 2025
239b6ba
chore: add secret key
wobsoriano Aug 4, 2025
a8d310f
chore: do not destructure body params in m2m endpoints
wobsoriano Aug 4, 2025
2a74cf9
chore: do not destructure body params in machine endpoints
wobsoriano Aug 4, 2025
60b139a
chore: update tests
wobsoriano Aug 4, 2025
ad9a0ec
chore: Use machine secret key from created Clerk client
wobsoriano Aug 4, 2025
8ca8009
chore: update missing clerk instance key or machine secret key error
wobsoriano Aug 4, 2025
7569d95
formatting
wobsoriano Aug 4, 2025
ad5a0f9
fix authenticate request option types
wobsoriano Aug 4, 2025
b72e891
fix assertion
wobsoriano Aug 4, 2025
2b2ce8e
Add machine secret key to merged options
wobsoriano Aug 4, 2025
afb5923
allow custom machine secret key per method
wobsoriano Aug 5, 2025
01653ae
add jsdoc
wobsoriano Aug 5, 2025
e05d414
chore: separate backend api client machine secret key and options sec…
wobsoriano Aug 5, 2025
85e47f1
chore: clean up authorizationHeader
wobsoriano Aug 5, 2025
e2ede5f
clean up authenticate context
wobsoriano Aug 6, 2025
c98d1ed
clean up authenticate context
wobsoriano Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hot-tables-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

WIP M2M Tokens
70 changes: 70 additions & 0 deletions packages/backend/src/api/endpoints/MachineApi.ts
Original file line number Diff line number Diff line change
@@ -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<PaginatedResourceResponse<Machine[]>>({
method: 'GET',
path: basePath,
queryParams,
});
}

async create(bodyParams: CreateMachineParams) {
return this.request<Machine>({
method: 'POST',
path: basePath,
bodyParams,
});
}

async update(params: UpdateMachineParams) {
const { machineId, ...bodyParams } = params;
this.requireId(machineId);
return this.request<Machine>({
method: 'PATCH',
path: joinPaths(basePath, machineId),
bodyParams,
});
}

async delete(params: DeleteMachineParams) {
const { machineId } = params;
this.requireId(machineId);
return this.request<Machine>({
method: 'DELETE',
path: joinPaths(basePath, machineId),
});
}

async get(machineId: string) {
this.requireId(machineId);
return this.request<Machine>({
method: 'GET',
path: joinPaths(basePath, machineId),
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider adding comprehensive tests for the new MachineApi.

This new API client provides full CRUD operations for machines and would benefit from test coverage to ensure all operations work correctly, including parameter validation and proper HTTP method usage.


🏁 Script executed:

#!/bin/bash
# Search for existing tests for MachineApi to verify test coverage
fd "MachineApi" --type f --extension ts --extension js | grep -i test

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Search within unit and integration test files for references to MachineApi
rg -n "MachineApi" --glob "*.{test,spec}.{ts,tsx,js}" .
rg -n "machine" --glob "*.{test,spec}.{ts,tsx,js}" .

Length of output: 13112


Add unit tests for MachineApi

No existing tests reference the new MachineApi class. Per our testing requirements, please add a Jest/Vitest unit-test suite covering all CRUD methods:

• Create a test file, e.g.
packages/backend/src/api/endpoints/tests/MachineApi.test.ts
• Mock the underlying HTTP request handler and verify:

  • list(queryParams) issues a GET to /machines with proper query params
  • create(body) issues a POST and correctly handles the response
  • update({ machineId, ... }) calls PATCH on /machines/:id, and throws when machineId is missing
  • delete({ machineId }) issues DELETE to /machines/:id, and throws when machineId is missing
  • get(id) issues GET to /machines/:id, and throws when id is missing
    • Assert correct typing of request and response payloads, and error handling
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/MachineApi.ts around lines 27 to 70, there
are no unit tests for the MachineApi class. Create a new test file named
MachineApi.test.ts in the __tests__ directory. Mock the HTTP request method used
in MachineApi and write tests for all CRUD methods: verify list sends a GET with
query params to /machines, create sends a POST with body and handles response,
update sends PATCH to /machines/:id and throws if machineId is missing, delete
sends DELETE to /machines/:id and throws if machineId is missing, and get sends
GET to /machines/:id and throws if id is missing. Also, assert correct typing of
request/response payloads and proper error handling in these tests.

108 changes: 102 additions & 6 deletions packages/backend/src/api/endpoints/MachineTokensApi.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,111 @@
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 WithMachineSecret<T> = T & { machineSecret?: string | null };

type CreateMachineTokenParams = WithMachineSecret<{
name: string;
subject: string;
claims?: Record<string, any> | null;
scopes?: string[];
createdBy?: string | null;
secondsUntilExpiration?: number | null;
}>;

type UpdateMachineTokenParams = WithMachineSecret<
{
m2mTokenId: string;
revoked?: boolean;
} & Pick<CreateMachineTokenParams, 'secondsUntilExpiration' | 'claims' | 'scopes'>
>;

type RevokeMachineTokenParams = WithMachineSecret<{
m2mTokenId: string;
revocationReason?: string | null;
}>;

type VerifyMachineTokenParams = WithMachineSecret<{
secret: string;
}>;

export class MachineTokensApi extends AbstractAPI {
async verifySecret(secret: string) {
return this.request<MachineToken>({
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { secret },
});
/**
* Overrides the instance secret with the machine secret.
*/
#withMachineSecretHeader(
options: ClerkBackendApiRequestOptions,
machineSecret?: string | null,
): ClerkBackendApiRequestOptions {
if (machineSecret) {
return {
...options,
headerParams: {
Authorization: `Bearer ${machineSecret}`,
},
};
}
return options;
}

async create(params: CreateMachineTokenParams) {
const { machineSecret, ...bodyParams } = params;
return this.request<MachineToken>(
this.#withMachineSecretHeader(
{
method: 'POST',
path: basePath,
bodyParams,
},
machineSecret,
),
);
}

async update(params: UpdateMachineTokenParams) {
const { m2mTokenId, machineSecret, ...bodyParams } = params;
this.requireId(m2mTokenId);
return this.request<MachineToken>(
this.#withMachineSecretHeader(
{
method: 'PATCH',
path: joinPaths(basePath, m2mTokenId),
bodyParams,
},
machineSecret,
),
);
}

async revoke(params: RevokeMachineTokenParams) {
const { m2mTokenId, machineSecret, ...bodyParams } = params;
this.requireId(m2mTokenId);
return this.request<MachineToken>(
this.#withMachineSecretHeader(
{
method: 'POST',
path: joinPaths(basePath, m2mTokenId, 'revoke'),
bodyParams,
},
machineSecret,
),
);
}

async verifySecret(params: VerifyMachineTokenParams) {
const { secret, machineSecret } = params;
return this.request<MachineToken>(
this.#withMachineSecretHeader(
{
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { secret },
},
machineSecret,
),
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Consider adding tests for the refactored API methods.

The significant refactoring of method signatures and addition of machine secret support would benefit from comprehensive test coverage to ensure the new parameter object pattern works correctly and machine secret authorization functions as expected.


🏁 Script executed:

#!/bin/bash
# Search for existing tests for MachineTokensApi to verify test coverage
fd "MachineTokensApi" --type f --extension ts --extension js | grep -i test

Length of output: 75


Let’s check for any references to MachineTokensApi within your test files:


🏁 Script executed:

#!/bin/bash
# Search for references to MachineTokensApi in all test files
rg --files-with-matches 'MachineTokensApi' -g '*.{test,spec}.{ts,tsx,js}'
rg --files-with-matches 'MachineTokensApi' -g '**/__tests__/**/*.{ts,tsx,js}'

Length of output: 151


Add unit tests for MachineTokensApi methods

The refactoring introduces new parameter patterns and machine-secret header logic that currently lack test coverage. Please add tests in packages/backend to ensure:

create, update, revoke, and verifySecret calls
• behavior with and without machineSecret
• correct override of headerParams.Authorization when machineSecret is provided
• preservation of existing headers when machineSecret is absent

🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/MachineTokensApi.ts from lines 35 to 111,
add unit tests for the MachineTokensApi class methods create, update, revoke,
and verifySecret. The tests should cover calls with and without the
machineSecret parameter, verify that when machineSecret is provided the
headerParams.Authorization is correctly overridden with the Bearer token, and
confirm that existing headers remain unchanged when machineSecret is not
provided. Ensure the tests validate the request options passed to the request
method to confirm the header behavior and parameter handling.

1 change: 1 addition & 0 deletions packages/backend/src/api/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
InvitationAPI,
JwksAPI,
JwtTemplatesApi,
MachineApi,
MachineTokensApi,
OAuthApplicationsApi,
OrganizationAPI,
Expand Down Expand Up @@ -64,10 +65,12 @@ 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,
skipApiVersionInUrl: true,
requireSecretKey: false,
}),
),
oauthApplications: new OAuthApplicationsApi(request),
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.set('Authorization', `Bearer ${secretKey}`);
if (secretKey && !headers.has(constants.Headers.Authorization)) {
headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`);
}

let res: Response | undefined;
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
InstanceSettings,
Invitation,
JwtTemplate,
Machine,
MachineToken,
OauthAccessToken,
OAuthApplication,
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -698,9 +699,19 @@ 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;
secret?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
15 changes: 15 additions & 0 deletions packages/backend/src/api/resources/Machine.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 3 additions & 1 deletion packages/backend/src/api/resources/MachineToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ export class MachineToken {
readonly creationReason: string | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly secret?: string,
) {}

static fromJSON(data: MachineTokenJSON) {
static fromJSON(data: MachineTokenJSON): MachineToken {
return new MachineToken(
data.id,
data.name,
Expand All @@ -32,6 +33,7 @@ export class MachineToken {
data.creation_reason,
data.created_at,
data.updated_at,
data.secret,
);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/tokens/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async function verifyMachineToken(
): Promise<MachineTokenReturnType<MachineToken, MachineTokenVerificationError>> {
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');
Expand Down