Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
const token = req.get('Authorization')?.split(' ')[1];

try {
const m2mToken = await clerkClient.m2mTokens.verifyToken({ token });
res.send('Protected response ' + m2mToken.id);
} catch {
res.status(401).send('Unauthorized');
}
});

app.get('/api/protected-deprecated', async (req, res) => {
const secret = req.get('Authorization')?.split(' ')[1];

try {
Expand Down Expand Up @@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All @@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All @@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All @@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
m2mTokenId: m2mToken.id,
});
});

test('authorizes M2M requests with deprecated verifySecret method', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected-deprecated', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
expect(await res.text()).toBe('Protected response ' + emailServerM2MToken.id);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand Down Expand Up @@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand Down Expand Up @@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
it('verifies a m2m token using machine secret', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
machineSecretKey: 'ak_xxxxx',
});

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(mockM2MToken);
}),
),
);

const response = await apiClient.m2mTokens.verifyToken({
token: m2mSecret,
});

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 () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_xxxxx',
});

server.use(
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.m2mTokens.verifyToken({
token: m2mSecret,
});

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 () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
});

server.use(
http.post(
'https://api.clerk.test/m2m_tokens/verify',
validateHeaders(() => {
return HttpResponse.json(mockM2MToken);
}),
),
);

const errResponse = await apiClient.m2mTokens
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

expect(errResponse.status).toBe(401);
});
});

describe('verifySecret (deprecated)', () => {
it('verifies a m2m token using machine secret', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All @@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
secret: 'mt_secret_test',
const response = await apiClient.m2mTokens.verifyToken({
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand Down Expand Up @@ -404,7 +404,7 @@ describe('api.client', () => {
expect(response.id).toBe('user_cafebabe');
});

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All @@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
secret: 'mt_secret_test',
const response = await apiClient.m2mTokens.verifyToken({
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand Down
39 changes: 37 additions & 2 deletions packages/backend/src/api/endpoints/M2MTokenApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { deprecated } from '@clerk/shared/deprecated';

import { joinPaths } from '../../util/path';
import type { ClerkBackendApiRequestOptions } from '../request';
import type { M2MToken } from '../resources/M2MToken';
Expand Down Expand Up @@ -31,7 +33,7 @@ type RevokeM2MTokenParams = {
revocationReason?: string | null;
};

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All @@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};

Comment on lines +47 to +57
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {
+export type VerifyM2MTokenParams = {
   /**
    * Custom machine secret key for authentication.
    */
   machineSecretKey?: string;
   /**
    * Machine-to-machine token to verify.
    */
   token: string;
 };
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand Down Expand Up @@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* Verify a machine-to-machine token.
*
* @deprecated Use {@link verifyToken} instead.
*/
async verifySecret(params: VerifyM2MTokenParamsDeprecated) {
const { secret, machineSecretKey } = params;

deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');

const requestOptions = this.#createRequestOptions(
Comment on lines +110 to 120
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

-  async verifySecret(params: VerifyM2MTokenParamsDeprecated) {
+  public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
     const { secret, machineSecretKey } = params;

-    deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
+    deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Verify a machine-to-machine token.
*
* @deprecated Use {@link verifyToken} instead.
*/
async verifySecret(params: VerifyM2MTokenParamsDeprecated) {
const { secret, machineSecretKey } = params;
deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
const requestOptions = this.#createRequestOptions(
/**
* Verify a machine-to-machine token.
*
* @deprecated Use {@link verifyToken} instead.
*/
public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
const requestOptions = this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All @@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

return this.request<M2MToken>(requestOptions);
}

async verifyToken(params: VerifyM2MTokenParams) {
const { token, machineSecretKey } = params;

const requestOptions = this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { token },
},
machineSecretKey,
);

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

-  async verifyToken(params: VerifyM2MTokenParams) {
+  /**
+   * Verify a machine-to-machine token.
+   *
+   * Verifies the provided M2M token against the Clerk API.
+   * By default, the client uses the configured Machine Secret Key.
+   * You can override it per-call by passing `machineSecretKey`.
+   *
+   * @param params - The verification parameters.
+   * @returns The verified M2MToken resource.
+   */
+  public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
     const { token, machineSecretKey } = params;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async verifyToken(params: VerifyM2MTokenParams) {
const { token, machineSecretKey } = params;
const requestOptions = this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { token },
},
machineSecretKey,
);
return this.request<M2MToken>(requestOptions);
}
/**
* Verify a machine-to-machine token.
*
* Verifies the provided M2M token against the Clerk API.
* By default, the client uses the configured Machine Secret Key.
* You can override it per-call by passing `machineSecretKey`.
*
* @param params - The verification parameters.
* @returns The verified M2MToken resource.
*/
public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
const requestOptions = this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { token },
},
machineSecretKey,
);
return this.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

-    readonly updatedAt: number,
-    readonly token?: string,
-    readonly secret?: string,
+    readonly updatedAt: number,
+    readonly secret?: string,
+    readonly token?: string,
@@
-      data.updated_at,
-      data.token,
-      data.secret,
+      data.updated_at,
+      data.secret,
+      data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All @@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand Down Expand Up @@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
+export async function verifyMachineAuthToken(
+  token: string,
+  options: VerifyTokenOptions,
+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
   if (token.startsWith(M2M_TOKEN_PREFIX)) {
     return verifyM2MToken(token, options);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
export async function verifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken | IdPOAuthAccessToken | APIKey,
MachineTokenVerificationError
>
> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading