diff --git a/integration/testUtils/index.ts b/integration/testUtils/index.ts index f694e9d4e2c..5c757e51b00 100644 --- a/integration/testUtils/index.ts +++ b/integration/testUtils/index.ts @@ -1,5 +1,4 @@ import { createClerkClient as backendCreateClerkClient } from '@clerk/backend'; -import { withRetry } from './retryableClerkClient'; import { createAppPageObject, createPageObjects, type EnhancedPage } from '@clerk/testing/playwright/unstable'; import type { Browser, BrowserContext, Page } from '@playwright/test'; @@ -7,18 +6,12 @@ import type { Application } from '../models/application'; import { createEmailService } from './emailService'; import { createInvitationService } from './invitationsService'; import { createOrganizationsService } from './organizationsService'; +import { withRetry } from './retryableClerkClient'; import type { FakeAPIKey, FakeOrganization, FakeUser, FakeUserWithEmail } from './usersService'; 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/machineAuthHelpers.ts b/integration/testUtils/machineAuthHelpers.ts new file mode 100644 index 00000000000..5582c67f2b8 --- /dev/null +++ b/integration/testUtils/machineAuthHelpers.ts @@ -0,0 +1,469 @@ +import { randomBytes } from 'node:crypto'; + +import type { ClerkClient, M2MToken, Machine, OAuthApplication, User } from '@clerk/backend'; +import { createClerkClient } from '@clerk/backend'; +import { TokenType } from '@clerk/backend/internal'; +import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import type { ApplicationConfig } from '../models/applicationConfig'; +import type { EnvironmentConfig } from '../models/environment'; +import { appConfigs } from '../presets'; +import { instanceKeys } from '../presets/envs'; +import type { FakeAPIKey, FakeUser } from './usersService'; +import { createTestUtils } from './index'; + +export type FakeMachineNetwork = { + primaryServer: Machine; + scopedSender: Machine; + unscopedSender: Machine; + scopedSenderToken: M2MToken; + unscopedSenderToken: M2MToken; + cleanup: () => Promise; +}; + +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 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), + ]); + }, + }; +} + +async function createJwtM2MToken(clerkClient: ClerkClient, senderSecretKey: string): Promise { + return clerkClient.m2m.createToken({ + machineSecretKey: senderSecretKey, + secondsUntilExpiration: 60 * 30, + tokenFormat: 'jwt', + }); +} + +export type FakeOAuthApp = { + oAuthApp: OAuthApplication; + cleanup: () => Promise; +}; + +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; + }; +}; + +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()); + + await signIn.waitForMounted(); + await signIn.signInWithEmailAndInstantPassword({ + email: fakeUser.email, + password: fakeUser.password, + }); + + const consentButton = page.getByRole('button', { name: 'Allow' }); + await consentButton.waitFor({ timeout: 10000 }); + await consentButton.click(); + + await page.waitForURL(/oauth\/callback/, { timeout: 10000 }); + const callbackUrl = new URL(page.url()); + const authCode = callbackUrl.searchParams.get('code'); + expect(authCode).toBeTruthy(); + + 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; +} + +type RouteBuilder = (config: ApplicationConfig) => ApplicationConfig; + +export type MachineAuthTestAdapter = { + baseConfig: ApplicationConfig; + apiKey: { + path: string; + addRoutes: RouteBuilder; + }; + m2m: { + path: string; + addRoutes: RouteBuilder; + }; + oauth: { + verifyPath: string; + callbackPath: string; + addRoutes: RouteBuilder; + }; +}; + +const createApiKeysEnv = (): EnvironmentConfig => appConfigs.envs.withAPIKeys.clone(); + +const createMachineClient = () => + createClerkClient({ + secretKey: instanceKeys.get('with-api-keys').sk, + }); + +const buildApp = async (adapter: MachineAuthTestAdapter, addRoutes: RouteBuilder): Promise => { + const config = addRoutes(adapter.baseConfig.clone()); + return config.commit(); +}; + +const createOAuthClient = (app: Application) => + createClerkClient({ + secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'), + publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'), + }); + +export const registerApiKeyAuthTests = (adapter: MachineAuthTestAdapter): void => { + 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 buildApp(adapter, adapter.apiKey.addRoutes); + await app.setup(); + await app.withEnv(createApiKeysEnv()); + 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 res = await request.get(new URL(adapter.apiKey.path, app.serverUrl).toString()); + expect(res.status()).toBe(401); + }); + + test('should return 401 if API key is invalid', async ({ request }) => { + const res = await request.get(new URL(adapter.apiKey.path, app.serverUrl).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 res = await request.get(new URL(adapter.apiKey.path, app.serverUrl).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 res = await request.get(new URL(adapter.apiKey.path, app.serverUrl).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(adapter.apiKey.path, app.serverUrl).toString(); + + 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(); + + const getRes = await u.page.request.get(url); + expect(getRes.status()).toBe(401); + + const postWithSessionRes = await u.page.request.post(url); + const sessionData = await postWithSessionRes.json(); + expect(postWithSessionRes.status()).toBe(200); + expect(sessionData.userId).toBe(fakeBapiUser.id); + expect(sessionData.tokenType).toBe(TokenType.SessionToken); + + const postWithApiKeyRes = await u.page.request.post(url, { + 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); + }); + }); +}; + +export const registerM2MAuthTests = (adapter: MachineAuthTestAdapter): void => { + test.describe('M2M auth', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let network: FakeMachineNetwork; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + network = await createFakeMachineNetwork(createMachineClient()); + app = await buildApp(adapter, adapter.m2m.addRoutes); + await app.setup(); + + const env = createApiKeysEnv().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 url = new URL(adapter.m2m.path, app.serverUrl).toString(); + const res = await request.get(url); + expect(res.status()).toBe(401); + + const res2 = await request.get(url, { + 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(new URL(adapter.m2m.path, app.serverUrl).toString(), { + 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(new URL(adapter.m2m.path, app.serverUrl).toString(), { + 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, + }); + + try { + const res = await u.page.request.get(new URL(adapter.m2m.path, app.serverUrl).toString(), { + 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); + } finally { + await u.services.clerk.m2m.revokeToken({ m2mTokenId: m2mToken.id }); + } + }); + + test('verifies JWT format M2M token via local verification', async ({ request }) => { + const jwtToken = await createJwtM2MToken(createMachineClient(), network.scopedSender.secretKey); + + const res = await request.get(new URL(adapter.m2m.path, app.serverUrl).toString(), { + 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(new URL(adapter.m2m.path, app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.status()).toBe(401); + }); + } + }); +}; + +export const registerOAuthAuthTests = (adapter: MachineAuthTestAdapter): void => { + 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 buildApp(adapter, adapter.oauth.addRoutes); + await app.setup(); + await app.withEnv(createApiKeysEnv()); + await app.dev(); + + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser(); + await u.services.users.createBapiUser(fakeUser); + fakeOAuth = await createFakeOAuthApp( + createOAuthClient(app), + new URL(adapter.oauth.callbackPath, app.serverUrl).toString(), + ); + }); + + 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: new URL(adapter.oauth.callbackPath, app.serverUrl).toString(), + fakeUser, + signIn: u.po.signIn, + }); + + const res = await u.page.request.get(new URL(adapter.oauth.verifyPath, 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 res = await request.get(new URL(adapter.oauth.verifyPath, app.serverUrl).toString()); + expect(res.status()).toBe(401); + }); + + test('rejects request with invalid OAuth token', async ({ request }) => { + const res = await request.get(new URL(adapter.oauth.verifyPath, app.serverUrl).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 res = await request.get(new URL(adapter.oauth.verifyPath, app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.status()).toBe(401); + }); + } + }); +}; diff --git a/integration/testUtils/machineAuthService.ts b/integration/testUtils/machineAuthService.ts deleted file mode 100644 index 3d0c3ca5047..00000000000 --- a/integration/testUtils/machineAuthService.ts +++ /dev/null @@ -1,186 +0,0 @@ -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 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), - ]); - }, - }; -} - -/** - * 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/astro/machine.test.ts b/integration/tests/astro/machine.test.ts index ca10578e3a8..613205986ef 100644 --- a/integration/tests/astro/machine.test.ts +++ b/integration/tests/astro/machine.test.ts @@ -1,223 +1,70 @@ -import type { User } from '@clerk/backend'; -import { createClerkClient } from '@clerk/backend'; -import { TokenType } from '@clerk/backend/internal'; -import { expect, test } from '@playwright/test'; +import { 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 type { MachineAuthTestAdapter } from '../../testUtils/machineAuthHelpers'; import { - createFakeMachineNetwork, - createFakeOAuthApp, - createJwtM2MToken, - createTestUtils, - obtainOAuthAccessToken, -} from '../../testUtils'; - -test.describe('Astro machine authentication @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 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); - }); - - 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.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.astro.node - .clone() - .addFile( - 'src/pages/api/m2m.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/m2m'); - expect(res.status()).toBe(401); - - 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/m2m', { - 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/m2m', { - 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); - - const res = await request.get(app.serverUrl + '/api/m2m', { - 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/m2m', { - 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.astro.node - .clone() + registerApiKeyAuthTests, + registerM2MAuthTests, + registerOAuthAuthTests, +} from '../../testUtils/machineAuthHelpers'; + +const adapter: MachineAuthTestAdapter = { + baseConfig: appConfigs.astro.node, + apiKey: { + path: '/api/me', + addRoutes: config => + config.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 new Response('Unauthorized', { status: 401 }); + } + + return Response.json({ userId, tokenType }); + }; + + export const POST: APIRoute = ({ locals }) => { + const authObject = locals.auth({ acceptsToken: ['api_key', 'session_token'] }); + + if (!authObject.isAuthenticated) { + return new Response('Unauthorized', { status: 401 }); + } + + return Response.json({ userId: authObject.userId, tokenType: authObject.tokenType }); + }; + `, + ), + }, + m2m: { + path: '/api/m2m', + addRoutes: config => + config.addFile( + 'src/pages/api/m2m.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 }); + }; + `, + ), + }, + oauth: { + verifyPath: '/api/oauth-verify', + callbackPath: '/api/oauth/callback', + addRoutes: config => + config .addFile( 'src/pages/api/oauth-verify.ts', () => ` @@ -243,76 +90,12 @@ test.describe('Astro machine authentication @machine', () => { 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/oauth-verify', 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/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/oauth-verify', 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/oauth-verify', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, - }); - expect(res.status()).toBe(401); - }); - } - }); +test.describe('Astro machine authentication @machine', () => { + registerApiKeyAuthTests(adapter); + registerM2MAuthTests(adapter); + registerOAuthAuthTests(adapter); }); diff --git a/integration/tests/next-machine.test.ts b/integration/tests/next-machine.test.ts index 31313e4d23b..0a753cce476 100644 --- a/integration/tests/next-machine.test.ts +++ b/integration/tests/next-machine.test.ts @@ -1,286 +1,66 @@ -import type { User } from '@clerk/backend'; -import { createClerkClient } from '@clerk/backend'; -import { TokenType } from '@clerk/backend/internal'; -import { expect, test } from '@playwright/test'; +import { 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 @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 }); +import type { MachineAuthTestAdapter } from '../testUtils/machineAuthHelpers'; +import { registerApiKeyAuthTests, registerM2MAuthTests, registerOAuthAuthTests } from '../testUtils/machineAuthHelpers'; + +const adapter: MachineAuthTestAdapter = { + baseConfig: appConfigs.next.appRouter, + apiKey: { + path: '/api/me', + addRoutes: config => + config.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 }); } - export async function POST() { - const authObject = await auth({ acceptsToken: ['api_key', 'session_token'] }); + return Response.json({ userId, tokenType }); + } - if (!authObject.isAuthenticated) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } + export async function POST() { + const authObject = await auth({ acceptsToken: ['api_key', 'session_token'] }); - return Response.json({ userId: authObject.userId, tokenType: authObject.tokenType }); + if (!authObject.isAuthenticated) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); } - `, - ) - .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 }); + return Response.json({ userId: authObject.userId, tokenType: authObject.tokenType }); + } + `, + ), + }, + m2m: { + path: '/api/protected', + addRoutes: config => + config.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 }); } - `, - ) - .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() + return Response.json({ subject, tokenType }); + } + `, + ), + }, + oauth: { + verifyPath: '/api/protected', + callbackPath: '/oauth/callback', + addRoutes: config => + config .addFile( 'src/app/api/protected/route.ts', () => ` @@ -306,76 +86,12 @@ test.describe('Next.js machine authentication @machine', () => { 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); - }); - } - }); +test.describe('Next.js machine authentication @machine', () => { + registerApiKeyAuthTests(adapter); + registerM2MAuthTests(adapter); + registerOAuthAuthTests(adapter); }); diff --git a/integration/tests/react-router/machine.test.ts b/integration/tests/react-router/machine.test.ts index 0b9b095fecc..ae909ccdf4a 100644 --- a/integration/tests/react-router/machine.test.ts +++ b/integration/tests/react-router/machine.test.ts @@ -1,33 +1,19 @@ -import type { User } from '@clerk/backend'; -import { createClerkClient } from '@clerk/backend'; -import { TokenType } from '@clerk/backend/internal'; -import { expect, test } from '@playwright/test'; +import { 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 type { MachineAuthTestAdapter } from '../../testUtils/machineAuthHelpers'; import { - createFakeMachineNetwork, - createFakeOAuthApp, - createJwtM2MToken, - createTestUtils, - obtainOAuthAccessToken, -} from '../../testUtils'; - -test.describe('React Router machine authentication @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.reactRouter.reactRouterNode - .clone() + registerApiKeyAuthTests, + registerM2MAuthTests, + registerOAuthAuthTests, +} from '../../testUtils/machineAuthHelpers'; + +const adapter: MachineAuthTestAdapter = { + baseConfig: appConfigs.reactRouter.reactRouterNode, + apiKey: { + path: '/api/me', + addRoutes: config => + config .addFile( 'app/routes/api/me.ts', () => ` @@ -43,6 +29,16 @@ test.describe('React Router machine authentication @machine', () => { return Response.json({ userId, tokenType }); } + + export async function action(args: Route.ActionArgs) { + const authObject = await getAuth(args, { acceptsToken: ['api_key', 'session_token'] }); + + if (!authObject.isAuthenticated) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ userId: authObject.userId, tokenType: authObject.tokenType }); + } `, ) .addFile( @@ -58,81 +54,12 @@ test.describe('React Router machine authentication @machine', () => { route('api/me', 'routes/api/me.ts'), ] satisfies RouteConfig; `, - ) - .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.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.reactRouter.reactRouterNode - .clone() + ), + }, + m2m: { + path: '/api/m2m', + addRoutes: config => + config .addFile( 'app/routes/api/m2m.ts', () => ` @@ -163,110 +90,13 @@ test.describe('React Router machine authentication @machine', () => { route('api/m2m', 'routes/api/m2m.ts'), ] satisfies RouteConfig; `, - ) - .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/m2m'); - expect(res.status()).toBe(401); - - 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/m2m', { - 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/m2m', { - 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/m2m', { - 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/m2m', { - 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/m2m', { - 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.reactRouter.reactRouterNode - .clone() + ), + }, + oauth: { + verifyPath: '/api/oauth-verify', + callbackPath: '/api/oauth/callback', + addRoutes: config => + config .addFile( 'app/routes/api/oauth-verify.ts', () => ` @@ -306,76 +136,12 @@ test.describe('React Router machine authentication @machine', () => { route('api/oauth/callback', 'routes/api/oauth-callback.ts'), ] satisfies RouteConfig; `, - ) - .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/oauth-verify', 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/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/oauth-verify', 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/oauth-verify', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, - }); - expect(res.status()).toBe(401); - }); - } - }); +test.describe('React Router machine authentication @machine', () => { + registerApiKeyAuthTests(adapter); + registerM2MAuthTests(adapter); + registerOAuthAuthTests(adapter); }); diff --git a/integration/tests/tanstack-start/machine.test.ts b/integration/tests/tanstack-start/machine.test.ts index 5fc4cfaa0df..7cc577b058c 100644 --- a/integration/tests/tanstack-start/machine.test.ts +++ b/integration/tests/tanstack-start/machine.test.ts @@ -1,256 +1,83 @@ -import type { User } from '@clerk/backend'; -import { createClerkClient } from '@clerk/backend'; -import { TokenType } from '@clerk/backend/internal'; -import { expect, test } from '@playwright/test'; +import { 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 type { MachineAuthTestAdapter } from '../../testUtils/machineAuthHelpers'; import { - createFakeMachineNetwork, - createFakeOAuthApp, - createJwtM2MToken, - createTestUtils, - obtainOAuthAccessToken, -} from '../../testUtils'; - -test.describe('TanStack React Start machine authentication @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.tanstack.reactStart - .clone() - .addFile( - 'src/routes/api/me.ts', - () => ` - import { createFileRoute } from '@tanstack/react-router' - import { auth } from '@clerk/tanstack-react-start/server' - - export const Route = createFileRoute('/api/me')({ - server: { - handlers: { - GET: async ({ request }) => { - const { userId, tokenType } = await auth({ acceptsToken: 'api_key' }); + registerApiKeyAuthTests, + registerM2MAuthTests, + registerOAuthAuthTests, +} from '../../testUtils/machineAuthHelpers'; + +const adapter: MachineAuthTestAdapter = { + baseConfig: appConfigs.tanstack.reactStart, + apiKey: { + path: '/api/me', + addRoutes: config => + config.addFile( + 'src/routes/api/me.ts', + () => ` + import { createFileRoute } from '@tanstack/react-router' + import { auth } from '@clerk/tanstack-react-start/server' + + export const Route = createFileRoute('/api/me')({ + server: { + handlers: { + GET: async () => { + const { userId, tokenType } = await auth({ acceptsToken: 'api_key' }); + + if (!userId) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ userId, tokenType }); + }, + POST: async () => { + const authObject = await auth({ acceptsToken: ['api_key', 'session_token'] }); - if (!userId) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } + if (!authObject.isAuthenticated) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } - return Response.json({ userId, tokenType }); - }, + 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.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.tanstack.reactStart - .clone() - .addFile( - 'src/routes/api/m2m.ts', - () => ` - import { createFileRoute } from '@tanstack/react-router' - import { auth } from '@clerk/tanstack-react-start/server' - - export const Route = createFileRoute('/api/m2m')({ - server: { - handlers: { - GET: async ({ request }) => { - const { subject, tokenType, isAuthenticated } = await auth({ acceptsToken: 'm2m_token' }); - - if (!isAuthenticated) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - return Response.json({ subject, tokenType }); - }, + }, + }) + `, + ), + }, + m2m: { + path: '/api/m2m', + addRoutes: config => + config.addFile( + 'src/routes/api/m2m.ts', + () => ` + import { createFileRoute } from '@tanstack/react-router' + import { auth } from '@clerk/tanstack-react-start/server' + + export const Route = createFileRoute('/api/m2m')({ + server: { + handlers: { + GET: async () => { + 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/m2m'); - expect(res.status()).toBe(401); - - 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/m2m', { - 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/m2m', { - 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/m2m', { - 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/m2m', { - 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/m2m', { - 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.tanstack.reactStart - .clone() + }, + }) + `, + ), + }, + oauth: { + verifyPath: '/api/oauth-verify', + callbackPath: '/api/oauth/callback', + addRoutes: config => + config .addFile( 'src/routes/api/oauth-verify.ts', () => ` @@ -260,7 +87,7 @@ test.describe('TanStack React Start machine authentication @machine', () => { export const Route = createFileRoute('/api/oauth-verify')({ server: { handlers: { - GET: async ({ request }) => { + GET: async () => { const { userId, tokenType } = await auth({ acceptsToken: 'oauth_token' }); if (!userId) { @@ -282,83 +109,19 @@ test.describe('TanStack React Start machine authentication @machine', () => { export const Route = createFileRoute('/api/oauth/callback')({ server: { handlers: { - GET: async ({ request }) => { + GET: async () => { 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/oauth-verify', 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/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/oauth-verify', 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/oauth-verify', app.serverUrl); - const res = await request.get(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, - }); - expect(res.status()).toBe(401); - }); - } - }); +test.describe('TanStack React Start machine authentication @machine', () => { + registerApiKeyAuthTests(adapter); + registerM2MAuthTests(adapter); + registerOAuthAuthTests(adapter); });