From a75d8c16b39c7d08ca0b03f46d69595be5abe8aa Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 19 Mar 2026 08:38:46 -0700 Subject: [PATCH 1/7] test(e2e): refactor machine auth tests for Next.js and Astro Restructure machine auth integration tests from shared test files into per-SDK test files for better isolation and maintainability. Add token type mismatch coverage to verify routes reject wrong token types (e.g., M2M token on API key route, API key on OAuth route). This is the first part of updating all machine auth tests across SDKs. Express will follow in a subsequent PR. --- integration/testUtils/index.ts | 7 + integration/testUtils/machineAuthService.ts | 182 ++++++++ ...ent.test.ts => api-keys-component.test.ts} | 8 +- integration/tests/astro/machine.test.ts | 433 ++++++++++++------ .../tests/machine-auth/api-keys.test.ts | 217 --------- integration/tests/machine-auth/m2m.test.ts | 193 -------- integration/tests/machine-auth/oauth.test.ts | 180 -------- integration/tests/next-machine.test.ts | 381 +++++++++++++++ 8 files changed, 864 insertions(+), 737 deletions(-) create mode 100644 integration/testUtils/machineAuthService.ts rename integration/tests/{machine-auth/component.test.ts => api-keys-component.test.ts} (99%) delete mode 100644 integration/tests/machine-auth/api-keys.test.ts delete mode 100644 integration/tests/machine-auth/m2m.test.ts delete mode 100644 integration/tests/machine-auth/oauth.test.ts create mode 100644 integration/tests/next-machine.test.ts diff --git a/integration/testUtils/index.ts b/integration/testUtils/index.ts index 310191b1938..f694e9d4e2c 100644 --- a/integration/testUtils/index.ts +++ b/integration/testUtils/index.ts @@ -12,6 +12,13 @@ import { createUserService } from './usersService'; import { createWaitlistService } from './waitlistService'; export type { FakeAPIKey, FakeOrganization, FakeUser, FakeUserWithEmail }; +export type { FakeMachineNetwork, FakeOAuthApp } from './machineAuthService'; +export { + createFakeMachineNetwork, + createFakeOAuthApp, + createJwtM2MToken, + obtainOAuthAccessToken, +} from './machineAuthService'; const createClerkClient = (app: Application) => { return backendCreateClerkClient({ diff --git a/integration/testUtils/machineAuthService.ts b/integration/testUtils/machineAuthService.ts new file mode 100644 index 00000000000..cc2208144d5 --- /dev/null +++ b/integration/testUtils/machineAuthService.ts @@ -0,0 +1,182 @@ +import { randomBytes } from 'node:crypto'; + +import type { ClerkClient, M2MToken, Machine, OAuthApplication } from '@clerk/backend'; +import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +// ─── M2M ──────────────────────────────────────────────────────────────────── + +export type FakeMachineNetwork = { + primaryServer: Machine; + scopedSender: Machine; + unscopedSender: Machine; + scopedSenderToken: M2MToken; + unscopedSenderToken: M2MToken; + cleanup: () => Promise; +}; + +/** + * Creates a network of three machines for M2M testing: + * - A primary API server (the "receiver") + * - A sender machine scoped to the primary (should succeed) + * - A sender machine with no scope (should fail) + * + * Each sender gets an opaque M2M token created for it. + * Call `cleanup()` to revoke tokens and delete all machines. + */ +export async function createFakeMachineNetwork(clerkClient: ClerkClient): Promise { + const fakeCompanyName = faker.company.name(); + + const primaryServer = await clerkClient.machines.create({ + name: `${fakeCompanyName} Primary API Server`, + }); + + const scopedSender = await clerkClient.machines.create({ + name: `${fakeCompanyName} Scoped Sender`, + scopedMachines: [primaryServer.id], + }); + const scopedSenderToken = await clerkClient.m2m.createToken({ + machineSecretKey: scopedSender.secretKey, + secondsUntilExpiration: 60 * 30, + }); + + const unscopedSender = await clerkClient.machines.create({ + name: `${fakeCompanyName} Unscoped Sender`, + }); + const unscopedSenderToken = await clerkClient.m2m.createToken({ + machineSecretKey: unscopedSender.secretKey, + secondsUntilExpiration: 60 * 30, + }); + + return { + primaryServer, + scopedSender, + unscopedSender, + scopedSenderToken, + unscopedSenderToken, + cleanup: async () => { + await clerkClient.m2m.revokeToken({ m2mTokenId: scopedSenderToken.id }); + await clerkClient.m2m.revokeToken({ m2mTokenId: unscopedSenderToken.id }); + await clerkClient.machines.delete(scopedSender.id); + await clerkClient.machines.delete(unscopedSender.id); + await clerkClient.machines.delete(primaryServer.id); + }, + }; +} + +/** + * Creates a JWT-format M2M token for a sender machine. + * JWT tokens are self-contained and expire via the `exp` claim (no revocation needed). + */ +export async function createJwtM2MToken(clerkClient: ClerkClient, senderSecretKey: string): Promise { + return clerkClient.m2m.createToken({ + machineSecretKey: senderSecretKey, + secondsUntilExpiration: 60 * 30, + tokenFormat: 'jwt', + }); +} + +// ─── OAuth ────────────────────────────────────────────────────────────────── + +export type FakeOAuthApp = { + oAuthApp: OAuthApplication; + cleanup: () => Promise; +}; + +/** + * Creates an OAuth application via BAPI for testing the full authorization code flow. + * Call `cleanup()` to delete the OAuth application. + */ +export async function createFakeOAuthApp(clerkClient: ClerkClient, callbackUrl: string): Promise { + const oAuthApp = await clerkClient.oauthApplications.create({ + name: `Integration Test OAuth App - ${Date.now()}`, + redirectUris: [callbackUrl], + scopes: 'profile email', + }); + + return { + oAuthApp, + cleanup: async () => { + await clerkClient.oauthApplications.delete(oAuthApp.id); + }, + }; +} + +export type ObtainOAuthAccessTokenParams = { + page: Page; + oAuthApp: OAuthApplication; + redirectUri: string; + fakeUser: { email?: string; password: string }; + signIn: { + waitForMounted: (...args: any[]) => Promise; + signInWithEmailAndInstantPassword: (params: { email: string; password: string }) => Promise; + }; +}; + +/** + * Runs the full OAuth 2.0 authorization code flow using Playwright: + * 1. Navigates to the authorize URL + * 2. Signs in with the provided user credentials + * 3. Accepts the consent screen + * 4. Extracts the authorization code from the callback + * 5. Exchanges the code for an access token + * + * Returns the access token string. + */ +export async function obtainOAuthAccessToken({ + page, + oAuthApp, + redirectUri, + fakeUser, + signIn, +}: ObtainOAuthAccessTokenParams): Promise { + const state = randomBytes(16).toString('hex'); + const authorizeUrl = new URL(oAuthApp.authorizeUrl); + authorizeUrl.searchParams.set('client_id', oAuthApp.clientId); + authorizeUrl.searchParams.set('redirect_uri', redirectUri); + authorizeUrl.searchParams.set('response_type', 'code'); + authorizeUrl.searchParams.set('scope', 'profile email'); + authorizeUrl.searchParams.set('state', state); + + await page.goto(authorizeUrl.toString()); + + // Sign in on Account Portal + await signIn.waitForMounted(); + await signIn.signInWithEmailAndInstantPassword({ + email: fakeUser.email!, + password: fakeUser.password, + }); + + // Accept consent screen + const consentButton = page.getByRole('button', { name: 'Allow' }); + await consentButton.waitFor({ timeout: 10000 }); + await consentButton.click(); + + // Wait for redirect and extract authorization code + await page.waitForURL(/oauth\/callback/, { timeout: 10000 }); + const callbackUrl = new URL(page.url()); + const authCode = callbackUrl.searchParams.get('code'); + expect(authCode).toBeTruthy(); + + // Exchange code for access token + expect(oAuthApp.clientSecret).toBeTruthy(); + const tokenResponse = await page.request.post(oAuthApp.tokenFetchUrl, { + data: new URLSearchParams({ + grant_type: 'authorization_code', + code: authCode!, + redirect_uri: redirectUri, + client_id: oAuthApp.clientId, + client_secret: oAuthApp.clientSecret!, + }).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + expect(tokenResponse.status()).toBe(200); + const tokenData = (await tokenResponse.json()) as { access_token?: string }; + expect(tokenData.access_token).toBeTruthy(); + + return tokenData.access_token!; +} diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/api-keys-component.test.ts similarity index 99% rename from integration/tests/machine-auth/component.test.ts rename to integration/tests/api-keys-component.test.ts index 989abe8145a..ef3354bff68 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/api-keys-component.test.ts @@ -1,10 +1,10 @@ import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; -import type { Application } from '../../models/application'; -import { appConfigs } from '../../presets'; -import type { FakeOrganization, FakeUser } from '../../testUtils'; -import { createTestUtils } from '../../testUtils'; +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import type { FakeOrganization, FakeUser } from '../testUtils'; +import { createTestUtils } from '../testUtils'; const mockAPIKeysEnvironmentSettings = async ( page: Page, diff --git a/integration/tests/astro/machine.test.ts b/integration/tests/astro/machine.test.ts index 58d0254f4b6..263115de4fa 100644 --- a/integration/tests/astro/machine.test.ts +++ b/integration/tests/astro/machine.test.ts @@ -1,171 +1,318 @@ import type { User } from '@clerk/backend'; +import { createClerkClient } from '@clerk/backend'; import { TokenType } from '@clerk/backend/internal'; import { expect, test } from '@playwright/test'; import type { Application } from '../../models/application'; import { appConfigs } from '../../presets'; -import type { FakeAPIKey, FakeUser } from '../../testUtils'; -import { createTestUtils } from '../../testUtils'; - -test.describe('Astro machine authentication within routes @machine', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - let fakeUser: FakeUser; - let fakeBapiUser: User; - let fakeAPIKey: FakeAPIKey; - - test.beforeAll(async () => { - test.setTimeout(90_000); // Wait for app to be ready - - app = await appConfigs.astro.node - .clone() - .addFile( - 'src/pages/api/auth/me.ts', - () => ` - import type { APIRoute } from 'astro'; - - const unautorized = () => - new Response('Unauthorized', { - status: 401, - }); - - export const GET: APIRoute = ({ locals }) => { +import { instanceKeys } from '../../presets/envs'; +import type { FakeAPIKey, FakeMachineNetwork, FakeOAuthApp, FakeUser } from '../../testUtils'; +import { + createFakeMachineNetwork, + createFakeOAuthApp, + createJwtM2MToken, + createTestUtils, + obtainOAuthAccessToken, +} from '../../testUtils'; + +test.describe('Astro machine authentication @astro @machine', () => { + test.describe('API key auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let fakeUser: FakeUser; + let fakeBapiUser: User; + let fakeAPIKey: FakeAPIKey; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + app = await appConfigs.astro.node + .clone() + .addFile( + 'src/pages/api/me.ts', + () => ` + import type { APIRoute } from 'astro'; + + export const GET: APIRoute = ({ locals }) => { const { userId, tokenType } = locals.auth({ acceptsToken: 'api_key' }); if (!userId) { - return unautorized(); + return new Response('Unauthorized', { status: 401 }); } return Response.json({ userId, tokenType }); - }; - `, - ) - .commit(); - - await app.setup(); - await app.withEnv(appConfigs.envs.withAPIKeys); - await app.dev(); - - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser(); - fakeBapiUser = await u.services.users.createBapiUser(fakeUser); - fakeAPIKey = await u.services.users.createFakeAPIKey(fakeBapiUser.id); - }); + }; + `, + ) + .commit(); - test.afterAll(async () => { - await fakeAPIKey.revoke(); - await fakeUser.deleteIfExists(); - await app.teardown(); - }); + await app.setup(); + await app.withEnv(appConfigs.envs.withAPIKeys); + await app.dev(); - test('should return 401 if no API key is provided', async ({ request }) => { - const url = new URL('/api/auth/me', app.serverUrl); - const res = await request.get(url.toString()); - expect(res.status()).toBe(401); - }); + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser(); + fakeBapiUser = await u.services.users.createBapiUser(fakeUser); + fakeAPIKey = await u.services.users.createFakeAPIKey(fakeBapiUser.id); + }); - test('should return 401 if API key is invalid', async ({ request }) => { - const url = new URL('/api/auth/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { Authorization: 'Bearer invalid_key' }, + test.afterAll(async () => { + await fakeAPIKey.revoke(); + await fakeUser.deleteIfExists(); + await app.teardown(); }); - expect(res.status()).toBe(401); - }); - test('should return 200 with auth object if API key is valid', async ({ request }) => { - const url = new URL('/api/auth/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { - Authorization: `Bearer ${fakeAPIKey.secret}`, - }, - }); - const apiKeyData = await res.json(); - expect(res.status()).toBe(200); - expect(apiKeyData.userId).toBe(fakeBapiUser.id); - expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); - }); -}); + test('should return 401 if no API key is provided', async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString()); + expect(res.status()).toBe(401); + }); + + test('should return 401 if API key is invalid', async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: 'Bearer invalid_key' }, + }); + expect(res.status()).toBe(401); + }); + + test('should return 200 with auth object if API key is valid', async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { + Authorization: `Bearer ${fakeAPIKey.secret}`, + }, + }); + const apiKeyData = await res.json(); + expect(res.status()).toBe(200); + expect(apiKeyData.userId).toBe(fakeBapiUser.id); + expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); + }); -test.describe('Astro machine authentication within clerkMiddleware() @machine', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - let fakeUser: FakeUser; - let fakeBapiUser: User; - let fakeAPIKey: FakeAPIKey; - - test.beforeAll(async () => { - test.setTimeout(90_000); // Wait for app to be ready - app = await appConfigs.astro.node - .clone() - .addFile( - `src/middleware.ts`, - () => ` - import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server'; - - const isProtectedRoute = createRouteMatcher(['/api(.*)']); - - export const onRequest = clerkMiddleware((auth, context) => { - const { userId } = auth({ acceptsToken: 'api_key' }) - - if (!userId && isProtectedRoute(context.request)) { - return new Response('Unauthorized', { status: 401 }); - } + for (const [tokenType, token] of [ + ['M2M', 'mt_test_mismatch'], + ['OAuth', 'oat_test_mismatch'], + ] as const) { + test(`rejects ${tokenType} token on API key route (token type mismatch)`, async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: `Bearer ${token}` }, }); - `, - ) - .addFile( - 'src/pages/api/auth/me.ts', - () => ` - import type { APIRoute } from 'astro'; - - export const GET: APIRoute = ({ locals, request }) => { - const { userId, tokenType } = locals.auth({ acceptsToken: 'api_key' }) - - return Response.json({ userId, tokenType }); - };`, - ) - .commit(); - - await app.setup(); - await app.withEnv(appConfigs.envs.withAPIKeys); - await app.dev(); - - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser(); - fakeBapiUser = await u.services.users.createBapiUser(fakeUser); - fakeAPIKey = await u.services.users.createFakeAPIKey(fakeBapiUser.id); + expect(res.status()).toBe(401); + }); + } }); - test.afterAll(async () => { - await fakeAPIKey.revoke(); - await fakeUser.deleteIfExists(); - await app.teardown(); - }); + test.describe('M2M auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let network: FakeMachineNetwork; - test('should return 401 if no API key is provided', async ({ request }) => { - const url = new URL('/api/auth/me', app.serverUrl); - const res = await request.get(url.toString()); - expect(res.status()).toBe(401); - }); + test.beforeAll(async () => { + test.setTimeout(120_000); + + const client = createClerkClient({ + secretKey: instanceKeys.get('with-api-keys').sk, + }); + network = await createFakeMachineNetwork(client); + + app = await appConfigs.astro.node + .clone() + .addFile( + 'src/pages/api/protected.ts', + () => ` + import type { APIRoute } from 'astro'; + + export const GET: APIRoute = ({ locals }) => { + const { subject, tokenType, isAuthenticated } = locals.auth({ acceptsToken: 'm2m_token' }); + + if (!isAuthenticated) { + return new Response('Unauthorized', { status: 401 }); + } + + return Response.json({ subject, tokenType }); + }; + `, + ) + .commit(); + + await app.setup(); + + const env = appConfigs.envs.withAPIKeys + .clone() + .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', network.primaryServer.secretKey); + await app.withEnv(env); + await app.dev(); + }); + + test.afterAll(async () => { + await network.cleanup(); + await app.teardown(); + }); + + test('rejects requests with invalid M2M tokens', async ({ request }) => { + const res = await request.get(app.serverUrl + '/api/protected'); + expect(res.status()).toBe(401); + + const res2 = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: 'Bearer mt_xxx' }, + }); + expect(res2.status()).toBe(401); + }); + + test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ request }) => { + const res = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${network.unscopedSenderToken.token}` }, + }); + expect(res.status()).toBe(401); + }); + + test('authorizes M2M requests when sender machine has proper access', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.scopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); + }); + + test('verifies JWT format M2M token via local verification', async ({ request }) => { + const client = createClerkClient({ + secretKey: instanceKeys.get('with-api-keys').sk, + }); + const jwtToken = await createJwtM2MToken(client, network.scopedSender.secretKey); - test('should return 401 if API key is invalid', async ({ request }) => { - const url = new URL('/api/auth/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { Authorization: 'Bearer invalid_key' }, + const res = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${jwtToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.scopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); }); - expect(res.status()).toBe(401); + + for (const [tokenType, token] of [ + ['API key', 'ak_test_mismatch'], + ['OAuth', 'oat_test_mismatch'], + ] as const) { + test(`rejects ${tokenType} token on M2M route (token type mismatch)`, async ({ request }) => { + const res = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.status()).toBe(401); + }); + } }); - test('should return 200 with auth object if API key is valid', async ({ request }) => { - const url = new URL('/api/auth/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { - Authorization: `Bearer ${fakeAPIKey.secret}`, - }, - }); - const apiKeyData = await res.json(); - expect(res.status()).toBe(200); - expect(apiKeyData.userId).toBe(fakeBapiUser.id); - expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); + test.describe('OAuth auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let fakeUser: FakeUser; + let fakeOAuth: FakeOAuthApp; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + app = await appConfigs.astro.node + .clone() + .addFile( + 'src/pages/api/protected.ts', + () => ` + import type { APIRoute } from 'astro'; + + export const GET: APIRoute = ({ locals }) => { + const { userId, tokenType } = locals.auth({ acceptsToken: 'oauth_token' }); + + if (!userId) { + return new Response('Unauthorized', { status: 401 }); + } + + return Response.json({ userId, tokenType }); + }; + `, + ) + .addFile( + 'src/pages/api/oauth/callback.ts', + () => ` + import type { APIRoute } from 'astro'; + + export const GET: APIRoute = () => { + return Response.json({ message: 'OAuth callback received' }); + }; + `, + ) + .commit(); + + await app.setup(); + await app.withEnv(appConfigs.envs.withAPIKeys); + await app.dev(); + + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser(); + await u.services.users.createBapiUser(fakeUser); + + const clerkClient = createClerkClient({ + secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'), + publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'), + }); + + fakeOAuth = await createFakeOAuthApp(clerkClient, `${app.serverUrl}/api/oauth/callback`); + }); + + test.afterAll(async () => { + await fakeOAuth.cleanup(); + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('verifies valid OAuth access token obtained through authorization flow', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const accessToken = await obtainOAuthAccessToken({ + page: u.page, + oAuthApp: fakeOAuth.oAuthApp, + redirectUri: `${app.serverUrl}/api/oauth/callback`, + fakeUser, + signIn: u.po.signIn, + }); + + const res = await u.page.request.get(new URL('/api/protected', app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + expect(res.status()).toBe(200); + const authData = await res.json(); + expect(authData.userId).toBeDefined(); + expect(authData.tokenType).toBe(TokenType.OAuthToken); + }); + + test('rejects request without OAuth token', async ({ request }) => { + const url = new URL('/api/protected', app.serverUrl); + const res = await request.get(url.toString()); + expect(res.status()).toBe(401); + }); + + test('rejects request with invalid OAuth token', async ({ request }) => { + const url = new URL('/api/protected', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: 'Bearer invalid_oauth_token' }, + }); + expect(res.status()).toBe(401); + }); + + for (const [tokenType, token] of [ + ['API key', 'ak_test_mismatch'], + ['M2M', 'mt_test_mismatch'], + ] as const) { + test(`rejects ${tokenType} token on OAuth route (token type mismatch)`, async ({ request }) => { + const url = new URL('/api/protected', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.status()).toBe(401); + }); + } }); }); diff --git a/integration/tests/machine-auth/api-keys.test.ts b/integration/tests/machine-auth/api-keys.test.ts deleted file mode 100644 index 73912c550e5..00000000000 --- a/integration/tests/machine-auth/api-keys.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import type { User } from '@clerk/backend'; -import { TokenType } from '@clerk/backend/internal'; -import { expect, test } from '@playwright/test'; - -import type { Application } from '../../models/application'; -import { appConfigs } from '../../presets'; -import type { FakeAPIKey, FakeUser } from '../../testUtils'; -import { createTestUtils } from '../../testUtils'; - -test.describe('Next.js API key auth within clerkMiddleware() @machine', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - let fakeUser: FakeUser; - let fakeBapiUser: User; - let fakeAPIKey: FakeAPIKey; - - test.beforeAll(async () => { - test.setTimeout(90_000); // Wait for app to be ready - app = await appConfigs.next.appRouter - .clone() - .addFile( - `src/middleware.ts`, - () => ` - import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; - - const isProtectedRoute = createRouteMatcher(['/api(.*)']); - - export default clerkMiddleware(async (auth, req) => { - if (isProtectedRoute(req)) { - await auth.protect({ token: 'api_key' }); - } - }); - - export const config = { - matcher: [ - '/((?!.*\\..*|_next).*)', // Don't run middleware on static files - '/', // Run middleware on index page - '/(api|trpc)(.*)', - ], // Run middleware on API routes - }; - `, - ) - .addFile( - 'src/app/api/me/route.ts', - () => ` - import { auth } from '@clerk/nextjs/server'; - - export async function GET() { - const { userId, tokenType } = await auth({ acceptsToken: 'api_key' }); - - return Response.json({ userId, tokenType }); - } - `, - ) - .commit(); - - await app.setup(); - await app.withEnv(appConfigs.envs.withAPIKeys); - await app.dev(); - - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser(); - fakeBapiUser = await u.services.users.createBapiUser(fakeUser); - fakeAPIKey = await u.services.users.createFakeAPIKey(fakeBapiUser.id); - }); - - test.afterAll(async () => { - await fakeAPIKey.revoke('Testing purposes within clerkMiddleware()'); - await fakeUser.deleteIfExists(); - await app.teardown(); - }); - - test('should return 401 if no API key is provided', async ({ request }) => { - const url = new URL('/api/me', app.serverUrl); - const res = await request.get(url.toString()); - expect(res.status()).toBe(401); - }); - - test('should return 401 if API key is invalid', async ({ request }) => { - const url = new URL('/api/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { Authorization: 'Bearer invalid_key' }, - }); - expect(res.status()).toBe(401); - }); - - test('should return 200 with auth object if API key is valid', async ({ request }) => { - const url = new URL('/api/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { - Authorization: `Bearer ${fakeAPIKey.secret}`, - }, - }); - const apiKeyData = await res.json(); - expect(res.status()).toBe(200); - expect(apiKeyData.userId).toBe(fakeBapiUser.id); - expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); - }); -}); - -test.describe('Next.js API key auth within routes @nextjs', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - let fakeUser: FakeUser; - let fakeBapiUser: User; - let fakeAPIKey: FakeAPIKey; - - test.beforeAll(async () => { - test.setTimeout(90_000); // Wait for app to be ready - - app = await appConfigs.next.appRouter - .clone() - .addFile( - 'src/app/api/me/route.ts', - () => ` - import { auth } from '@clerk/nextjs/server'; - - export async function GET() { - const { userId, tokenType } = await auth({ acceptsToken: 'api_key' }); - - if (!userId) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - return Response.json({ userId, tokenType }); - } - - export async function POST() { - const authObject = await auth({ acceptsToken: ['api_key', 'session_token'] }); - - if (!authObject.isAuthenticated) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - return Response.json({ userId: authObject.userId, tokenType: authObject.tokenType }); - } - `, - ) - .commit(); - - await app.setup(); - await app.withEnv(appConfigs.envs.withAPIKeys); - await app.dev(); - - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser(); - fakeBapiUser = await u.services.users.createBapiUser(fakeUser); - fakeAPIKey = await u.services.users.createFakeAPIKey(fakeBapiUser.id); - }); - - test.afterAll(async () => { - await fakeAPIKey.revoke(); - await fakeUser.deleteIfExists(); - await app.teardown(); - }); - - test('should return 401 if no API key is provided', async ({ request }) => { - const url = new URL('/api/me', app.serverUrl); - const res = await request.get(url.toString()); - expect(res.status()).toBe(401); - }); - - test('should return 401 if API key is invalid', async ({ request }) => { - const url = new URL('/api/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { Authorization: 'Bearer invalid_key' }, - }); - expect(res.status()).toBe(401); - }); - - test('should return 200 with auth object if API key is valid', async ({ request }) => { - const url = new URL('/api/me', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { - Authorization: `Bearer ${fakeAPIKey.secret}`, - }, - }); - const apiKeyData = await res.json(); - expect(res.status()).toBe(200); - expect(apiKeyData.userId).toBe(fakeBapiUser.id); - expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); - }); - - test('should handle multiple token types', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const url = new URL('/api/me', app.serverUrl); - - // Sign in to get a session token - await u.po.signIn.goTo(); - await u.po.signIn.waitForMounted(); - await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); - await u.po.expect.toBeSignedIn(); - - // GET endpoint (only accepts api_key) - const getRes = await u.page.request.get(url.toString()); - expect(getRes.status()).toBe(401); - - // POST endpoint (accepts both api_key and session_token) - // Test with session token - const postWithSessionRes = await u.page.request.post(url.toString()); - const sessionData = await postWithSessionRes.json(); - expect(postWithSessionRes.status()).toBe(200); - expect(sessionData.userId).toBe(fakeBapiUser.id); - expect(sessionData.tokenType).toBe(TokenType.SessionToken); - - // Test with API key - const postWithApiKeyRes = await u.page.request.post(url.toString(), { - headers: { - Authorization: `Bearer ${fakeAPIKey.secret}`, - }, - }); - const apiKeyData = await postWithApiKeyRes.json(); - expect(postWithApiKeyRes.status()).toBe(200); - expect(apiKeyData.userId).toBe(fakeBapiUser.id); - expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); - }); -}); diff --git a/integration/tests/machine-auth/m2m.test.ts b/integration/tests/machine-auth/m2m.test.ts deleted file mode 100644 index 73721cebc53..00000000000 --- a/integration/tests/machine-auth/m2m.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { createClerkClient, type M2MToken, type Machine } from '@clerk/backend'; -import { faker } from '@faker-js/faker'; -import { expect, test } from '@playwright/test'; - -import type { Application } from '../../models/application'; -import { appConfigs } from '../../presets'; -import { instanceKeys } from '../../presets/envs'; -import { createTestUtils } from '../../testUtils'; - -test.describe('machine-to-machine auth @machine', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - let primaryApiServer: Machine; - let emailServer: Machine; - let analyticsServer: Machine; - let emailServerM2MToken: M2MToken; - let analyticsServerM2MToken: M2MToken; - - test.beforeAll(async () => { - test.setTimeout(90_000); // Wait for app to be ready - const fakeCompanyName = faker.company.name(); - - // Create primary machine using instance secret key - const client = createClerkClient({ - secretKey: instanceKeys.get('with-api-keys').sk, - }); - primaryApiServer = await client.machines.create({ - name: `${fakeCompanyName} Primary API Server`, - }); - - app = await appConfigs.express.vite - .clone() - .addFile( - 'src/server/main.ts', - () => ` - import 'dotenv/config'; - import { clerkClient } from '@clerk/express'; - import express from 'express'; - import ViteExpress from 'vite-express'; - - const app = express(); - - app.get('/api/protected', async (req, res) => { - const token = req.get('Authorization')?.split(' ')[1]; - try { - const m2mToken = await clerkClient.m2m.verify({ token }); - res.send('Protected response ' + m2mToken.subject); - } catch { - res.status(401).send('Unauthorized'); - } - }); - - const port = parseInt(process.env.PORT as string) || 3002; - ViteExpress.listen(app, port, () => console.log('Server started')); - `, - ) - .commit(); - - await app.setup(); - - // Using the created machine, set a machine secret key using the primary machine's secret key - const env = appConfigs.envs.withAPIKeys - .clone() - .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', primaryApiServer.secretKey); - await app.withEnv(env); - await app.dev(); - - // Email server can access primary API server - emailServer = await client.machines.create({ - name: `${fakeCompanyName} Email Server`, - scopedMachines: [primaryApiServer.id], - }); - emailServerM2MToken = await client.m2m.createToken({ - machineSecretKey: emailServer.secretKey, - secondsUntilExpiration: 60 * 30, - }); - - // Analytics server cannot access primary API server - analyticsServer = await client.machines.create({ - name: `${fakeCompanyName} Analytics Server`, - // No scoped machines - }); - analyticsServerM2MToken = await client.m2m.createToken({ - machineSecretKey: analyticsServer.secretKey, - secondsUntilExpiration: 60 * 30, - }); - }); - - test.afterAll(async () => { - const client = createClerkClient({ - secretKey: instanceKeys.get('with-api-keys').sk, - }); - - await client.m2m.revokeToken({ - m2mTokenId: emailServerM2MToken.id, - }); - await client.m2m.revokeToken({ - m2mTokenId: analyticsServerM2MToken.id, - }); - await client.machines.delete(emailServer.id); - await client.machines.delete(primaryApiServer.id); - await client.machines.delete(analyticsServer.id); - - await app.teardown(); - }); - - test('rejects requests with invalid M2M tokens', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { - Authorization: `Bearer invalid`, - }, - }); - expect(res.status()).toBe(401); - expect(await res.text()).toBe('Unauthorized'); - - const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { - Authorization: `Bearer mt_xxx`, - }, - }); - expect(res2.status()).toBe(401); - expect(await res2.text()).toBe('Unauthorized'); - }); - - test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { - Authorization: `Bearer ${analyticsServerM2MToken.token}`, - }, - }); - expect(res.status()).toBe(401); - expect(await res.text()).toBe('Unauthorized'); - }); - - test('authorizes M2M requests when sender machine has proper access to receiver machine', 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', { - headers: { - Authorization: `Bearer ${emailServerM2MToken.token}`, - }, - }); - expect(res.status()).toBe(200); - expect(await res.text()).toBe('Protected response ' + emailServer.id); - - // Analytics server can access primary API server after adding scope - await u.services.clerk.machines.createScope(analyticsServer.id, primaryApiServer.id); - const m2mToken = await u.services.clerk.m2m.createToken({ - machineSecretKey: analyticsServer.secretKey, - secondsUntilExpiration: 60 * 30, - }); - - const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { - Authorization: `Bearer ${m2mToken.token}`, - }, - }); - expect(res2.status()).toBe(200); - expect(await res2.text()).toBe('Protected response ' + analyticsServer.id); - await u.services.clerk.m2m.revokeToken({ - m2mTokenId: m2mToken.id, - }); - }); - - test('verifies JWT format M2M token via local verification', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - const client = createClerkClient({ - secretKey: instanceKeys.get('with-api-keys').sk, - }); - const jwtToken = await client.m2m.createToken({ - machineSecretKey: emailServer.secretKey, - secondsUntilExpiration: 60 * 30, - tokenFormat: 'jwt', - }); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { Authorization: `Bearer ${jwtToken.token}` }, - }); - expect(res.status()).toBe(200); - expect(await res.text()).toBe('Protected response ' + emailServer.id); - // JWT-format tokens are self-contained and not stored in BAPI, so revocation - // is not applicable — they expire naturally via the exp claim. - }); -}); diff --git a/integration/tests/machine-auth/oauth.test.ts b/integration/tests/machine-auth/oauth.test.ts deleted file mode 100644 index a501a15bfad..00000000000 --- a/integration/tests/machine-auth/oauth.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { randomBytes } from 'node:crypto'; - -import type { OAuthApplication } from '@clerk/backend'; -import { createClerkClient } from '@clerk/backend'; -import { TokenType } from '@clerk/backend/internal'; -import { expect, test } from '@playwright/test'; - -import type { Application } from '../../models/application'; -import { appConfigs } from '../../presets'; -import type { FakeUser } from '../../testUtils'; -import { createTestUtils } from '../../testUtils'; - -test.describe('OAuth machine authentication @machine', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - let fakeUser: FakeUser; - let oAuthApp: OAuthApplication; - - test.beforeAll(async () => { - test.setTimeout(120_000); - - app = await appConfigs.next.appRouter - .clone() - .addFile( - 'src/app/api/protected/route.ts', - () => ` - import { auth } from '@clerk/nextjs/server'; - - export async function GET() { - const { userId, tokenType } = await auth({ acceptsToken: 'oauth_token' }); - - if (!userId) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - return Response.json({ userId, tokenType }); - } - `, - ) - .addFile( - 'src/app/oauth/callback/route.ts', - () => ` - import { NextResponse } from 'next/server'; - - export async function GET() { - return NextResponse.json({ message: 'OAuth callback received' }); - } - `, - ) - .commit(); - - await app.setup(); - await app.withEnv(appConfigs.envs.withAPIKeys); - await app.dev(); - - // Test user that will authorize the OAuth application - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser(); - await u.services.users.createBapiUser(fakeUser); - - const clerkClient = createClerkClient({ - secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'), - publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'), - }); - - // Create an OAuth application via the BAPI - oAuthApp = await clerkClient.oauthApplications.create({ - name: `Integration Test OAuth App - ${Date.now()}`, - redirectUris: [`${app.serverUrl}/oauth/callback`], - scopes: 'profile email', - }); - }); - - test.afterAll(async () => { - const clerkClient = createClerkClient({ - secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'), - publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'), - }); - - if (oAuthApp.id) { - await clerkClient.oauthApplications.delete(oAuthApp.id); - } - - await fakeUser.deleteIfExists(); - await app.teardown(); - }); - - test('verifies valid OAuth access token obtained through authorization flow', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - // Build the authorization URL - const state = randomBytes(16).toString('hex'); - const redirectUri = `${app.serverUrl}/oauth/callback`; - const authorizeUrl = new URL(oAuthApp.authorizeUrl); - authorizeUrl.searchParams.set('client_id', oAuthApp.clientId); - authorizeUrl.searchParams.set('redirect_uri', redirectUri); - authorizeUrl.searchParams.set('response_type', 'code'); - authorizeUrl.searchParams.set('scope', 'profile email'); - authorizeUrl.searchParams.set('state', state); - - // Navigate to Clerk's authorization endpoint - await u.page.goto(authorizeUrl.toString()); - - // Sign in on Account Portal - await u.po.signIn.waitForMounted(); - await u.po.signIn.signInWithEmailAndInstantPassword({ - email: fakeUser.email, - password: fakeUser.password, - }); - - // Accept consent screen - // Per https://clerk.com/docs/guides/configure/auth-strategies/oauth/how-clerk-implements-oauth#consent-screen-management - const consentButton = u.page.getByRole('button', { name: 'Allow' }); - await consentButton.waitFor({ timeout: 10000 }); - await consentButton.click(); - - // Wait for the redirect to complete - await u.page.waitForURL(/oauth\/callback/, { timeout: 10000 }); - - // Extract the authorization code from the callback URL - const currentUrl = u.page.url(); - const urlObj = new URL(currentUrl); - const finalAuthCode = urlObj.searchParams.get('code'); - - expect(finalAuthCode).toBeTruthy(); - - // Exchange authorization code for access token - expect(oAuthApp.clientSecret).toBeTruthy(); - - const tokenResponse = await u.page.request.post(oAuthApp.tokenFetchUrl, { - data: new URLSearchParams({ - grant_type: 'authorization_code', - code: finalAuthCode, - redirect_uri: redirectUri, - client_id: oAuthApp.clientId, - client_secret: oAuthApp.clientSecret, - }).toString(), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - - expect(tokenResponse.status()).toBe(200); - const tokenResponseBody = await tokenResponse.text(); - - const tokenData = JSON.parse(tokenResponseBody) as { access_token?: string }; - const accessToken = tokenData.access_token; - - expect(accessToken).toBeTruthy(); - - // Use the access token to authenticate a request to our protected route - const protectedRouteUrl = new URL('/api/protected', app.serverUrl); - const protectedResponse = await u.page.request.get(protectedRouteUrl.toString(), { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - expect(protectedResponse.status()).toBe(200); - const authData = await protectedResponse.json(); - expect(authData.userId).toBeDefined(); - expect(authData.tokenType).toBe(TokenType.OAuthToken); - }); - - test('rejects request without OAuth token', async ({ request }) => { - const url = new URL('/api/protected', app.serverUrl); - const res = await request.get(url.toString()); - expect(res.status()).toBe(401); - }); - - test('rejects request with invalid OAuth token', async ({ request }) => { - const url = new URL('/api/protected', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { - Authorization: 'Bearer invalid_oauth_token', - }, - }); - expect(res.status()).toBe(401); - }); -}); diff --git a/integration/tests/next-machine.test.ts b/integration/tests/next-machine.test.ts new file mode 100644 index 00000000000..1583d04fb86 --- /dev/null +++ b/integration/tests/next-machine.test.ts @@ -0,0 +1,381 @@ +import type { User } from '@clerk/backend'; +import { createClerkClient } from '@clerk/backend'; +import { TokenType } from '@clerk/backend/internal'; +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import { instanceKeys } from '../presets/envs'; +import type { FakeAPIKey, FakeMachineNetwork, FakeOAuthApp, FakeUser } from '../testUtils'; +import { + createFakeMachineNetwork, + createFakeOAuthApp, + createJwtM2MToken, + createTestUtils, + obtainOAuthAccessToken, +} from '../testUtils'; + +test.describe('Next.js machine authentication @nextjs @machine', () => { + test.describe('API key auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let fakeUser: FakeUser; + let fakeBapiUser: User; + let fakeAPIKey: FakeAPIKey; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/app/api/me/route.ts', + () => ` + import { auth } from '@clerk/nextjs/server'; + + export async function GET() { + const { userId, tokenType } = await auth({ acceptsToken: 'api_key' }); + + if (!userId) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ userId, tokenType }); + } + + export async function POST() { + const authObject = await auth({ acceptsToken: ['api_key', 'session_token'] }); + + if (!authObject.isAuthenticated) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ userId: authObject.userId, tokenType: authObject.tokenType }); + } + `, + ) + .commit(); + + await app.setup(); + await app.withEnv(appConfigs.envs.withAPIKeys); + await app.dev(); + + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser(); + fakeBapiUser = await u.services.users.createBapiUser(fakeUser); + fakeAPIKey = await u.services.users.createFakeAPIKey(fakeBapiUser.id); + }); + + test.afterAll(async () => { + await fakeAPIKey.revoke(); + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('should return 401 if no API key is provided', async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString()); + expect(res.status()).toBe(401); + }); + + test('should return 401 if API key is invalid', async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: 'Bearer invalid_key' }, + }); + expect(res.status()).toBe(401); + }); + + test('should return 200 with auth object if API key is valid', async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { + Authorization: `Bearer ${fakeAPIKey.secret}`, + }, + }); + const apiKeyData = await res.json(); + expect(res.status()).toBe(200); + expect(apiKeyData.userId).toBe(fakeBapiUser.id); + expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); + }); + + for (const [tokenType, token] of [ + ['M2M', 'mt_test_mismatch'], + ['OAuth', 'oat_test_mismatch'], + ] as const) { + test(`rejects ${tokenType} token on API key route (token type mismatch)`, async ({ request }) => { + const url = new URL('/api/me', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.status()).toBe(401); + }); + } + + test('should handle multiple token types', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + const url = new URL('/api/me', app.serverUrl); + + // Sign in to get a session token + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + // GET endpoint (only accepts api_key) - session token should fail + const getRes = await u.page.request.get(url.toString()); + expect(getRes.status()).toBe(401); + + // POST endpoint (accepts both api_key and session_token) + // Test with session token + const postWithSessionRes = await u.page.request.post(url.toString()); + const sessionData = await postWithSessionRes.json(); + expect(postWithSessionRes.status()).toBe(200); + expect(sessionData.userId).toBe(fakeBapiUser.id); + expect(sessionData.tokenType).toBe(TokenType.SessionToken); + + // Test with API key + const postWithApiKeyRes = await u.page.request.post(url.toString(), { + headers: { + Authorization: `Bearer ${fakeAPIKey.secret}`, + }, + }); + const apiKeyData = await postWithApiKeyRes.json(); + expect(postWithApiKeyRes.status()).toBe(200); + expect(apiKeyData.userId).toBe(fakeBapiUser.id); + expect(apiKeyData.tokenType).toBe(TokenType.ApiKey); + }); + }); + + test.describe('M2M auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let network: FakeMachineNetwork; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + const client = createClerkClient({ + secretKey: instanceKeys.get('with-api-keys').sk, + }); + network = await createFakeMachineNetwork(client); + + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/app/api/protected/route.ts', + () => ` + import { auth } from '@clerk/nextjs/server'; + + export async function GET() { + const { subject, tokenType, isAuthenticated } = await auth({ acceptsToken: 'm2m_token' }); + + if (!isAuthenticated) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ subject, tokenType }); + } + `, + ) + .commit(); + + await app.setup(); + + const env = appConfigs.envs.withAPIKeys + .clone() + .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', network.primaryServer.secretKey); + await app.withEnv(env); + await app.dev(); + }); + + test.afterAll(async () => { + await network.cleanup(); + await app.teardown(); + }); + + test('rejects requests with invalid M2M tokens', async ({ request }) => { + const res = await request.get(app.serverUrl + '/api/protected'); + expect(res.status()).toBe(401); + + const res2 = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: 'Bearer mt_xxx' }, + }); + expect(res2.status()).toBe(401); + }); + + test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ request }) => { + const res = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${network.unscopedSenderToken.token}` }, + }); + expect(res.status()).toBe(401); + }); + + test('authorizes M2M requests when sender machine has proper access', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.scopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); + }); + + test('authorizes after dynamically granting scope', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.services.clerk.machines.createScope(network.unscopedSender.id, network.primaryServer.id); + const m2mToken = await u.services.clerk.m2m.createToken({ + machineSecretKey: network.unscopedSender.secretKey, + secondsUntilExpiration: 60 * 30, + }); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${m2mToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.unscopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); + await u.services.clerk.m2m.revokeToken({ m2mTokenId: m2mToken.id }); + }); + + test('verifies JWT format M2M token via local verification', async ({ request }) => { + const client = createClerkClient({ + secretKey: instanceKeys.get('with-api-keys').sk, + }); + const jwtToken = await createJwtM2MToken(client, network.scopedSender.secretKey); + + const res = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${jwtToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.scopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); + }); + + for (const [tokenType, token] of [ + ['API key', 'ak_test_mismatch'], + ['OAuth', 'oat_test_mismatch'], + ] as const) { + test(`rejects ${tokenType} token on M2M route (token type mismatch)`, async ({ request }) => { + const res = await request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.status()).toBe(401); + }); + } + }); + + test.describe('OAuth auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let fakeUser: FakeUser; + let fakeOAuth: FakeOAuthApp; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/app/api/protected/route.ts', + () => ` + import { auth } from '@clerk/nextjs/server'; + + export async function GET() { + const { userId, tokenType } = await auth({ acceptsToken: 'oauth_token' }); + + if (!userId) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ userId, tokenType }); + } + `, + ) + .addFile( + 'src/app/oauth/callback/route.ts', + () => ` + import { NextResponse } from 'next/server'; + + export async function GET() { + return NextResponse.json({ message: 'OAuth callback received' }); + } + `, + ) + .commit(); + + await app.setup(); + await app.withEnv(appConfigs.envs.withAPIKeys); + await app.dev(); + + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser(); + await u.services.users.createBapiUser(fakeUser); + + const clerkClient = createClerkClient({ + secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'), + publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'), + }); + + fakeOAuth = await createFakeOAuthApp(clerkClient, `${app.serverUrl}/oauth/callback`); + }); + + test.afterAll(async () => { + await fakeOAuth.cleanup(); + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('verifies valid OAuth access token obtained through authorization flow', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const accessToken = await obtainOAuthAccessToken({ + page: u.page, + oAuthApp: fakeOAuth.oAuthApp, + redirectUri: `${app.serverUrl}/oauth/callback`, + fakeUser, + signIn: u.po.signIn, + }); + + const res = await u.page.request.get(new URL('/api/protected', app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + expect(res.status()).toBe(200); + const authData = await res.json(); + expect(authData.userId).toBeDefined(); + expect(authData.tokenType).toBe(TokenType.OAuthToken); + }); + + test('rejects request without OAuth token', async ({ request }) => { + const url = new URL('/api/protected', app.serverUrl); + const res = await request.get(url.toString()); + expect(res.status()).toBe(401); + }); + + test('rejects request with invalid OAuth token', async ({ request }) => { + const url = new URL('/api/protected', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: 'Bearer invalid_oauth_token' }, + }); + expect(res.status()).toBe(401); + }); + + for (const [tokenType, token] of [ + ['API key', 'ak_test_mismatch'], + ['M2M', 'mt_test_mismatch'], + ] as const) { + test(`rejects ${tokenType} token on OAuth route (token type mismatch)`, async ({ request }) => { + const url = new URL('/api/protected', app.serverUrl); + const res = await request.get(url.toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.status()).toBe(401); + }); + } + }); +}); From 70a506339f4ddf9bd7d94916165b0e743c4e26a0 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 19 Mar 2026 08:42:41 -0700 Subject: [PATCH 2/7] chore: remove extra tags --- integration/tests/astro/machine.test.ts | 2 +- integration/tests/express/machine.test.ts | 138 ++++++++++++++++++++++ integration/tests/next-machine.test.ts | 2 +- 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 integration/tests/express/machine.test.ts diff --git a/integration/tests/astro/machine.test.ts b/integration/tests/astro/machine.test.ts index 263115de4fa..a7ff8c5a8a5 100644 --- a/integration/tests/astro/machine.test.ts +++ b/integration/tests/astro/machine.test.ts @@ -15,7 +15,7 @@ import { obtainOAuthAccessToken, } from '../../testUtils'; -test.describe('Astro machine authentication @astro @machine', () => { +test.describe('Astro machine authentication @machine', () => { test.describe('API key auth', () => { test.describe.configure({ mode: 'parallel' }); let app: Application; diff --git a/integration/tests/express/machine.test.ts b/integration/tests/express/machine.test.ts new file mode 100644 index 00000000000..5c581759933 --- /dev/null +++ b/integration/tests/express/machine.test.ts @@ -0,0 +1,138 @@ +import { createClerkClient } from '@clerk/backend'; +import { TokenType } from '@clerk/backend/internal'; +import { expect, test } from '@playwright/test'; + +import type { Application } from '../../models/application'; +import { appConfigs } from '../../presets'; +import { instanceKeys } from '../../presets/envs'; +import type { FakeMachineNetwork } from '../../testUtils'; +import { createFakeMachineNetwork, createJwtM2MToken, createTestUtils } from '../../testUtils'; + +test.describe('Express machine authentication @express @machine', () => { + test.describe('M2M auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let network: FakeMachineNetwork; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + const client = createClerkClient({ + secretKey: instanceKeys.get('with-api-keys').sk, + }); + network = await createFakeMachineNetwork(client); + + app = await appConfigs.express.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { clerkClient } from '@clerk/express'; + import express from 'express'; + import ViteExpress from 'vite-express'; + + const app = express(); + + app.get('/api/protected', async (req, res) => { + const token = req.get('Authorization')?.split(' ')[1]; + try { + const m2mToken = await clerkClient.m2m.verify({ token }); + res.json({ subject: m2mToken.subject, tokenType: '${TokenType.M2MToken}' }); + } catch { + res.status(401).send('Unauthorized'); + } + }); + + const port = parseInt(process.env.PORT as string) || 3002; + ViteExpress.listen(app, port, () => console.log('Server started')); + `, + ) + .commit(); + + await app.setup(); + + const env = appConfigs.envs.withAPIKeys + .clone() + .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', network.primaryServer.secretKey); + await app.withEnv(env); + await app.dev(); + }); + + test.afterAll(async () => { + await network.cleanup(); + await app.teardown(); + }); + + test('rejects requests with invalid M2M tokens', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: 'Bearer invalid' }, + }); + expect(res.status()).toBe(401); + + const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: 'Bearer mt_xxx' }, + }); + expect(res2.status()).toBe(401); + }); + + test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${network.unscopedSenderToken.token}` }, + }); + expect(res.status()).toBe(401); + }); + + test('authorizes M2M requests when sender machine has proper access', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.scopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); + }); + + test('authorizes after dynamically granting scope', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.services.clerk.machines.createScope(network.unscopedSender.id, network.primaryServer.id); + const m2mToken = await u.services.clerk.m2m.createToken({ + machineSecretKey: network.unscopedSender.secretKey, + secondsUntilExpiration: 60 * 30, + }); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${m2mToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.unscopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); + await u.services.clerk.m2m.revokeToken({ m2mTokenId: m2mToken.id }); + }); + + test('verifies JWT format M2M token via local verification', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const client = createClerkClient({ + secretKey: instanceKeys.get('with-api-keys').sk, + }); + const jwtToken = await createJwtM2MToken(client, network.scopedSender.secretKey); + + const res = await u.page.request.get(app.serverUrl + '/api/protected', { + headers: { Authorization: `Bearer ${jwtToken.token}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.subject).toBe(network.scopedSender.id); + expect(body.tokenType).toBe(TokenType.M2MToken); + }); + }); +}); diff --git a/integration/tests/next-machine.test.ts b/integration/tests/next-machine.test.ts index 1583d04fb86..31313e4d23b 100644 --- a/integration/tests/next-machine.test.ts +++ b/integration/tests/next-machine.test.ts @@ -15,7 +15,7 @@ import { obtainOAuthAccessToken, } from '../testUtils'; -test.describe('Next.js machine authentication @nextjs @machine', () => { +test.describe('Next.js machine authentication @machine', () => { test.describe('API key auth', () => { test.describe.configure({ mode: 'parallel' }); let app: Application; From a9f8580da9bb1265eb06f5793edce5acf4229c9a Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 19 Mar 2026 09:01:43 -0700 Subject: [PATCH 3/7] chore: remove express machine test from this branch Express machine auth tests will be added in a follow-up PR. --- integration/tests/express/machine.test.ts | 138 ---------------------- 1 file changed, 138 deletions(-) delete mode 100644 integration/tests/express/machine.test.ts diff --git a/integration/tests/express/machine.test.ts b/integration/tests/express/machine.test.ts deleted file mode 100644 index 5c581759933..00000000000 --- a/integration/tests/express/machine.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { createClerkClient } from '@clerk/backend'; -import { TokenType } from '@clerk/backend/internal'; -import { expect, test } from '@playwright/test'; - -import type { Application } from '../../models/application'; -import { appConfigs } from '../../presets'; -import { instanceKeys } from '../../presets/envs'; -import type { FakeMachineNetwork } from '../../testUtils'; -import { createFakeMachineNetwork, createJwtM2MToken, createTestUtils } from '../../testUtils'; - -test.describe('Express machine authentication @express @machine', () => { - test.describe('M2M auth', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - let network: FakeMachineNetwork; - - test.beforeAll(async () => { - test.setTimeout(120_000); - - const client = createClerkClient({ - secretKey: instanceKeys.get('with-api-keys').sk, - }); - network = await createFakeMachineNetwork(client); - - app = await appConfigs.express.vite - .clone() - .addFile( - 'src/server/main.ts', - () => ` - import 'dotenv/config'; - import { clerkClient } from '@clerk/express'; - import express from 'express'; - import ViteExpress from 'vite-express'; - - const app = express(); - - app.get('/api/protected', async (req, res) => { - const token = req.get('Authorization')?.split(' ')[1]; - try { - const m2mToken = await clerkClient.m2m.verify({ token }); - res.json({ subject: m2mToken.subject, tokenType: '${TokenType.M2MToken}' }); - } catch { - res.status(401).send('Unauthorized'); - } - }); - - const port = parseInt(process.env.PORT as string) || 3002; - ViteExpress.listen(app, port, () => console.log('Server started')); - `, - ) - .commit(); - - await app.setup(); - - const env = appConfigs.envs.withAPIKeys - .clone() - .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', network.primaryServer.secretKey); - await app.withEnv(env); - await app.dev(); - }); - - test.afterAll(async () => { - await network.cleanup(); - await app.teardown(); - }); - - test('rejects requests with invalid M2M tokens', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { Authorization: 'Bearer invalid' }, - }); - expect(res.status()).toBe(401); - - const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { Authorization: 'Bearer mt_xxx' }, - }); - expect(res2.status()).toBe(401); - }); - - test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { Authorization: `Bearer ${network.unscopedSenderToken.token}` }, - }); - expect(res.status()).toBe(401); - }); - - test('authorizes M2M requests when sender machine has proper access', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.subject).toBe(network.scopedSender.id); - expect(body.tokenType).toBe(TokenType.M2MToken); - }); - - test('authorizes after dynamically granting scope', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - await u.services.clerk.machines.createScope(network.unscopedSender.id, network.primaryServer.id); - const m2mToken = await u.services.clerk.m2m.createToken({ - machineSecretKey: network.unscopedSender.secretKey, - secondsUntilExpiration: 60 * 30, - }); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { Authorization: `Bearer ${m2mToken.token}` }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.subject).toBe(network.unscopedSender.id); - expect(body.tokenType).toBe(TokenType.M2MToken); - await u.services.clerk.m2m.revokeToken({ m2mTokenId: m2mToken.id }); - }); - - test('verifies JWT format M2M token via local verification', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - const client = createClerkClient({ - secretKey: instanceKeys.get('with-api-keys').sk, - }); - const jwtToken = await createJwtM2MToken(client, network.scopedSender.secretKey); - - const res = await u.page.request.get(app.serverUrl + '/api/protected', { - headers: { Authorization: `Bearer ${jwtToken.token}` }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.subject).toBe(network.scopedSender.id); - expect(body.tokenType).toBe(TokenType.M2MToken); - }); - }); -}); From d1e6035f47eb9ef1c5e2b8da880656bf8e9a76b9 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 19 Mar 2026 09:30:35 -0700 Subject: [PATCH 4/7] chore: clean up promises --- integration/testUtils/machineAuthService.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/integration/testUtils/machineAuthService.ts b/integration/testUtils/machineAuthService.ts index cc2208144d5..d1f783680df 100644 --- a/integration/testUtils/machineAuthService.ts +++ b/integration/testUtils/machineAuthService.ts @@ -56,11 +56,13 @@ export async function createFakeMachineNetwork(clerkClient: ClerkClient): Promis scopedSenderToken, unscopedSenderToken, cleanup: async () => { - await clerkClient.m2m.revokeToken({ m2mTokenId: scopedSenderToken.id }); - await clerkClient.m2m.revokeToken({ m2mTokenId: unscopedSenderToken.id }); - await clerkClient.machines.delete(scopedSender.id); - await clerkClient.machines.delete(unscopedSender.id); - await clerkClient.machines.delete(primaryServer.id); + await Promise.all([ + await clerkClient.m2m.revokeToken({ m2mTokenId: scopedSenderToken.id }), + await clerkClient.m2m.revokeToken({ m2mTokenId: unscopedSenderToken.id }), + await clerkClient.machines.delete(scopedSender.id), + await clerkClient.machines.delete(unscopedSender.id), + await clerkClient.machines.delete(primaryServer.id), + ]); }, }; } @@ -144,7 +146,7 @@ export async function obtainOAuthAccessToken({ // Sign in on Account Portal await signIn.waitForMounted(); await signIn.signInWithEmailAndInstantPassword({ - email: fakeUser.email!, + email: fakeUser.email, password: fakeUser.password, }); @@ -164,10 +166,10 @@ export async function obtainOAuthAccessToken({ const tokenResponse = await page.request.post(oAuthApp.tokenFetchUrl, { data: new URLSearchParams({ grant_type: 'authorization_code', - code: authCode!, + code: authCode, redirect_uri: redirectUri, client_id: oAuthApp.clientId, - client_secret: oAuthApp.clientSecret!, + client_secret: oAuthApp.clientSecret, }).toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -178,5 +180,5 @@ export async function obtainOAuthAccessToken({ const tokenData = (await tokenResponse.json()) as { access_token?: string }; expect(tokenData.access_token).toBeTruthy(); - return tokenData.access_token!; + return tokenData.access_token; } From 5804ee0b2424f98db21e1d00677e2001fb1078a9 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 19 Mar 2026 10:08:02 -0700 Subject: [PATCH 5/7] test: fix protected route --- integration/tests/astro/machine.test.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/integration/tests/astro/machine.test.ts b/integration/tests/astro/machine.test.ts index a7ff8c5a8a5..ca10578e3a8 100644 --- a/integration/tests/astro/machine.test.ts +++ b/integration/tests/astro/machine.test.ts @@ -119,7 +119,7 @@ test.describe('Astro machine authentication @machine', () => { app = await appConfigs.astro.node .clone() .addFile( - 'src/pages/api/protected.ts', + 'src/pages/api/m2m.ts', () => ` import type { APIRoute } from 'astro'; @@ -151,17 +151,17 @@ test.describe('Astro machine authentication @machine', () => { }); test('rejects requests with invalid M2M tokens', async ({ request }) => { - const res = await request.get(app.serverUrl + '/api/protected'); + const res = await request.get(app.serverUrl + '/api/m2m'); expect(res.status()).toBe(401); - const res2 = await request.get(app.serverUrl + '/api/protected', { + const res2 = await request.get(app.serverUrl + '/api/m2m', { headers: { Authorization: 'Bearer mt_xxx' }, }); expect(res2.status()).toBe(401); }); test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ request }) => { - const res = await request.get(app.serverUrl + '/api/protected', { + const res = await request.get(app.serverUrl + '/api/m2m', { headers: { Authorization: `Bearer ${network.unscopedSenderToken.token}` }, }); expect(res.status()).toBe(401); @@ -170,7 +170,7 @@ test.describe('Astro machine authentication @machine', () => { test('authorizes M2M requests when sender machine has proper access', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); - const res = await u.page.request.get(app.serverUrl + '/api/protected', { + const res = await u.page.request.get(app.serverUrl + '/api/m2m', { headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` }, }); expect(res.status()).toBe(200); @@ -185,7 +185,7 @@ test.describe('Astro machine authentication @machine', () => { }); const jwtToken = await createJwtM2MToken(client, network.scopedSender.secretKey); - const res = await request.get(app.serverUrl + '/api/protected', { + const res = await request.get(app.serverUrl + '/api/m2m', { headers: { Authorization: `Bearer ${jwtToken.token}` }, }); expect(res.status()).toBe(200); @@ -199,7 +199,7 @@ test.describe('Astro machine authentication @machine', () => { ['OAuth', 'oat_test_mismatch'], ] as const) { test(`rejects ${tokenType} token on M2M route (token type mismatch)`, async ({ request }) => { - const res = await request.get(app.serverUrl + '/api/protected', { + const res = await request.get(app.serverUrl + '/api/m2m', { headers: { Authorization: `Bearer ${token}` }, }); expect(res.status()).toBe(401); @@ -219,7 +219,7 @@ test.describe('Astro machine authentication @machine', () => { app = await appConfigs.astro.node .clone() .addFile( - 'src/pages/api/protected.ts', + 'src/pages/api/oauth-verify.ts', () => ` import type { APIRoute } from 'astro'; @@ -279,7 +279,7 @@ test.describe('Astro machine authentication @machine', () => { signIn: u.po.signIn, }); - const res = await u.page.request.get(new URL('/api/protected', app.serverUrl).toString(), { + const res = await u.page.request.get(new URL('/api/oauth-verify', app.serverUrl).toString(), { headers: { Authorization: `Bearer ${accessToken}` }, }); expect(res.status()).toBe(200); @@ -289,13 +289,13 @@ test.describe('Astro machine authentication @machine', () => { }); test('rejects request without OAuth token', async ({ request }) => { - const url = new URL('/api/protected', app.serverUrl); + const url = new URL('/api/oauth-verify', app.serverUrl); const res = await request.get(url.toString()); expect(res.status()).toBe(401); }); test('rejects request with invalid OAuth token', async ({ request }) => { - const url = new URL('/api/protected', app.serverUrl); + const url = new URL('/api/oauth-verify', app.serverUrl); const res = await request.get(url.toString(), { headers: { Authorization: 'Bearer invalid_oauth_token' }, }); @@ -307,7 +307,7 @@ test.describe('Astro machine authentication @machine', () => { ['M2M', 'mt_test_mismatch'], ] as const) { test(`rejects ${tokenType} token on OAuth route (token type mismatch)`, async ({ request }) => { - const url = new URL('/api/protected', app.serverUrl); + const url = new URL('/api/oauth-verify', app.serverUrl); const res = await request.get(url.toString(), { headers: { Authorization: `Bearer ${token}` }, }); From 78b502c8c4a5e2e6a2e4066f19ca10939a703c8d Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 19 Mar 2026 10:08:45 -0700 Subject: [PATCH 6/7] chore: fix incorrect await usage --- integration/testUtils/machineAuthService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/integration/testUtils/machineAuthService.ts b/integration/testUtils/machineAuthService.ts index d1f783680df..271c8315132 100644 --- a/integration/testUtils/machineAuthService.ts +++ b/integration/testUtils/machineAuthService.ts @@ -57,11 +57,11 @@ export async function createFakeMachineNetwork(clerkClient: ClerkClient): Promis unscopedSenderToken, cleanup: async () => { await Promise.all([ - await clerkClient.m2m.revokeToken({ m2mTokenId: scopedSenderToken.id }), - await clerkClient.m2m.revokeToken({ m2mTokenId: unscopedSenderToken.id }), - await clerkClient.machines.delete(scopedSender.id), - await clerkClient.machines.delete(unscopedSender.id), - await clerkClient.machines.delete(primaryServer.id), + clerkClient.m2m.revokeToken({ m2mTokenId: scopedSenderToken.id }), + clerkClient.m2m.revokeToken({ m2mTokenId: unscopedSenderToken.id }), + clerkClient.machines.delete(scopedSender.id), + clerkClient.machines.delete(unscopedSender.id), + clerkClient.machines.delete(primaryServer.id), ]); }, }; From d151d2f377b777712c78b55bdaaa35137c3b69a6 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 20 Mar 2026 09:10:27 -0700 Subject: [PATCH 7/7] Update integration/testUtils/machineAuthService.ts Co-authored-by: Tom Milewski --- integration/testUtils/machineAuthService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration/testUtils/machineAuthService.ts b/integration/testUtils/machineAuthService.ts index 271c8315132..3d0c3ca5047 100644 --- a/integration/testUtils/machineAuthService.ts +++ b/integration/testUtils/machineAuthService.ts @@ -59,6 +59,8 @@ export async function createFakeMachineNetwork(clerkClient: ClerkClient): Promis await Promise.all([ clerkClient.m2m.revokeToken({ m2mTokenId: scopedSenderToken.id }), clerkClient.m2m.revokeToken({ m2mTokenId: unscopedSenderToken.id }), + ]); + await Promise.all([ clerkClient.machines.delete(scopedSender.id), clerkClient.machines.delete(unscopedSender.id), clerkClient.machines.delete(primaryServer.id),