diff --git a/.changeset/small-adults-crash.md b/.changeset/small-adults-crash.md new file mode 100644 index 00000000000..dffb1925224 --- /dev/null +++ b/.changeset/small-adults-crash.md @@ -0,0 +1,5 @@ +--- +"@clerk/backend": minor +--- + +Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })` diff --git a/integration/tests/machine-auth/m2m.test.ts b/integration/tests/machine-auth/m2m.test.ts index c495a1b26f9..0eb98458b4f 100644 --- a/integration/tests/machine-auth/m2m.test.ts +++ b/integration/tests/machine-auth/m2m.test.ts @@ -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 { @@ -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); @@ -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); @@ -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); @@ -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); + }); }); diff --git a/packages/backend/src/api/__tests__/M2MTokenApi.test.ts b/packages/backend/src/api/__tests__/M2MTokenApi.test.ts index c73a49fa1f4..e4d597380d5 100644 --- a/packages/backend/src/api/__tests__/M2MTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/M2MTokenApi.test.ts @@ -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, @@ -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' }); }); @@ -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', diff --git a/packages/backend/src/api/__tests__/factory.test.ts b/packages/backend/src/api/__tests__/factory.test.ts index fdeaae4eea2..7ed0cdb19a1 100644 --- a/packages/backend/src/api/__tests__/factory.test.ts +++ b/packages/backend/src/api/__tests__/factory.test.ts @@ -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'); }); @@ -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'); }); @@ -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', @@ -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'); }); diff --git a/packages/backend/src/api/endpoints/M2MTokenApi.ts b/packages/backend/src/api/endpoints/M2MTokenApi.ts index 379f4f13ffd..4689e5d2724 100644 --- a/packages/backend/src/api/endpoints/M2MTokenApi.ts +++ b/packages/backend/src/api/endpoints/M2MTokenApi.ts @@ -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'; @@ -31,7 +33,7 @@ type RevokeM2MTokenParams = { revocationReason?: string | null; }; -type VerifyM2MTokenParams = { +type VerifyM2MTokenParamsDeprecated = { /** * Custom machine secret key for authentication. */ @@ -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; +}; + export class M2MTokenApi extends AbstractAPI { #createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) { if (machineSecretKey) { @@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI { return this.request(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( { method: 'POST', @@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI { return this.request(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(requestOptions); + } } diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 06fef96008f..3d19a564794 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -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 | null; diff --git a/packages/backend/src/api/resources/M2MToken.ts b/packages/backend/src/api/resources/M2MToken.ts index 97358e79745..0e5457a3a71 100644 --- a/packages/backend/src/api/resources/M2MToken.ts +++ b/packages/backend/src/api/resources/M2MToken.ts @@ -12,6 +12,7 @@ export class M2MToken { readonly expiration: number | null, readonly createdAt: number, readonly updatedAt: number, + readonly token?: string, readonly secret?: string, ) {} @@ -27,6 +28,7 @@ export class M2MToken { data.expiration, data.created_at, data.updated_at, + data.token, data.secret, ); } diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index ad8597f19da..32a9803c7ca 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -200,13 +200,13 @@ function handleClerkAPIError( }; } -async function verifyMachineToken( - secret: string, +async function verifyM2MToken( + token: string, options: VerifyTokenOptions & { machineSecretKey?: string }, ): Promise> { 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'); @@ -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); } if (token.startsWith(OAUTH_TOKEN_PREFIX)) { return verifyOAuthToken(token, options);