From b033fb4d02ccf201e5277bbd8cf8dec48a820784 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 24 Mar 2026 06:46:26 -0700 Subject: [PATCH 1/2] test(e2e): add machine auth tests for Express, Fastify, and Hono --- integration/tests/express/machine.test.ts | 352 +++++++++++++++++ integration/tests/fastify/machine.test.ts | 442 ++++++++++++++++++++++ integration/tests/hono/machine.test.ts | 370 ++++++++++++++++++ 3 files changed, 1164 insertions(+) create mode 100644 integration/tests/express/machine.test.ts create mode 100644 integration/tests/fastify/machine.test.ts create mode 100644 integration/tests/hono/machine.test.ts diff --git a/integration/tests/express/machine.test.ts b/integration/tests/express/machine.test.ts new file mode 100644 index 00000000000..ed5f0f7c8a6 --- /dev/null +++ b/integration/tests/express/machine.test.ts @@ -0,0 +1,352 @@ +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('Express 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.express.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { clerkMiddleware, getAuth } from '@clerk/express'; + import express from 'express'; + import ViteExpress from 'vite-express'; + + const app = express(); + app.use(clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); + + app.get('/api/me', (req, res) => { + const { userId, tokenType } = getAuth(req, { acceptsToken: 'api_key' }); + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + return res.json({ userId, tokenType }); + }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(app, port, () => console.log('Server is listening on port ' + port)); + `, + ) + .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.express.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { clerkMiddleware, getAuth } from '@clerk/express'; + import express from 'express'; + import ViteExpress from 'vite-express'; + + const app = express(); + app.use(clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); + + app.get('/api/m2m', (req, res) => { + const { subject, tokenType, isAuthenticated } = getAuth(req, { acceptsToken: 'm2m_token' }); + if (!isAuthenticated) { + return res.status(401).json({ error: 'Unauthorized' }); + } + return res.json({ subject, tokenType }); + }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(app, port, () => console.log('Server is listening on port ' + port)); + `, + ) + .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.express.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { clerkMiddleware, getAuth } from '@clerk/express'; + import express from 'express'; + import ViteExpress from 'vite-express'; + + const app = express(); + app.use(clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); + + app.get('/api/oauth-verify', (req, res) => { + const { userId, tokenType } = getAuth(req, { acceptsToken: 'oauth_token' }); + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + return res.json({ userId, tokenType }); + }); + + app.get('/api/oauth/callback', (req, res) => { + return res.json({ message: 'OAuth callback received' }); + }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(app, port, () => console.log('Server is listening on port ' + port)); + `, + ) + .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); + }); + } + }); +}); diff --git a/integration/tests/fastify/machine.test.ts b/integration/tests/fastify/machine.test.ts new file mode 100644 index 00000000000..51a3e55676f --- /dev/null +++ b/integration/tests/fastify/machine.test.ts @@ -0,0 +1,442 @@ +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('Fastify 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.fastify.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { clerkPlugin, getAuth } from '@clerk/fastify'; + import express from 'express'; + import Fastify from 'fastify'; + import ViteExpress from 'vite-express'; + + async function start() { + const fastify = Fastify(); + fastify.register(clerkPlugin, { publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY }); + + fastify.get('/me', async (request, reply) => { + const { userId, tokenType } = getAuth(request, { acceptsToken: 'api_key' }); + if (!userId) { + return reply.code(401).send({ error: 'Unauthorized' }); + } + return reply.send({ userId, tokenType }); + }); + + await fastify.listen({ port: 0, host: '127.0.0.1' }); + const fastifyAddress = fastify.server.address(); + const fastifyPort = typeof fastifyAddress === 'object' ? fastifyAddress?.port : 0; + + const expressApp = express(); + expressApp.use('/api', async (req, res) => { + const url = 'http://127.0.0.1:' + fastifyPort + req.url; + const headers = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (typeof value === 'string') headers[key] = value; + else if (Array.isArray(value)) headers[key] = value.join(', '); + } + const response = await fetch(url, { + method: req.method, + headers, + body: ['GET', 'HEAD'].includes(req.method) ? undefined : req, + duplex: ['GET', 'HEAD'].includes(req.method) ? undefined : 'half', + redirect: 'manual', + }); + res.status(response.status); + response.headers.forEach((value, key) => { res.setHeader(key, value); }); + const body = await response.arrayBuffer(); + res.send(Buffer.from(body)); + }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); + } + + start(); + `, + ) + .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.fastify.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { clerkPlugin, getAuth } from '@clerk/fastify'; + import express from 'express'; + import Fastify from 'fastify'; + import ViteExpress from 'vite-express'; + + async function start() { + const fastify = Fastify(); + fastify.register(clerkPlugin, { publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY }); + + fastify.get('/m2m', async (request, reply) => { + const { subject, tokenType, isAuthenticated } = getAuth(request, { acceptsToken: 'm2m_token' }); + if (!isAuthenticated) { + return reply.code(401).send({ error: 'Unauthorized' }); + } + return reply.send({ subject, tokenType }); + }); + + await fastify.listen({ port: 0, host: '127.0.0.1' }); + const fastifyAddress = fastify.server.address(); + const fastifyPort = typeof fastifyAddress === 'object' ? fastifyAddress?.port : 0; + + const expressApp = express(); + expressApp.use('/api', async (req, res) => { + const url = 'http://127.0.0.1:' + fastifyPort + req.url; + const headers = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (typeof value === 'string') headers[key] = value; + else if (Array.isArray(value)) headers[key] = value.join(', '); + } + const response = await fetch(url, { + method: req.method, + headers, + body: ['GET', 'HEAD'].includes(req.method) ? undefined : req, + duplex: ['GET', 'HEAD'].includes(req.method) ? undefined : 'half', + redirect: 'manual', + }); + res.status(response.status); + response.headers.forEach((value, key) => { res.setHeader(key, value); }); + const body = await response.arrayBuffer(); + res.send(Buffer.from(body)); + }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); + } + + start(); + `, + ) + .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.fastify.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { clerkPlugin, getAuth } from '@clerk/fastify'; + import express from 'express'; + import Fastify from 'fastify'; + import ViteExpress from 'vite-express'; + + async function start() { + const fastify = Fastify(); + fastify.register(clerkPlugin, { publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY }); + + fastify.get('/oauth-verify', async (request, reply) => { + const { userId, tokenType } = getAuth(request, { acceptsToken: 'oauth_token' }); + if (!userId) { + return reply.code(401).send({ error: 'Unauthorized' }); + } + return reply.send({ userId, tokenType }); + }); + + fastify.get('/oauth/callback', async (request, reply) => { + return reply.send({ message: 'OAuth callback received' }); + }); + + await fastify.listen({ port: 0, host: '127.0.0.1' }); + const fastifyAddress = fastify.server.address(); + const fastifyPort = typeof fastifyAddress === 'object' ? fastifyAddress?.port : 0; + + const expressApp = express(); + expressApp.use('/api', async (req, res) => { + const url = 'http://127.0.0.1:' + fastifyPort + req.url; + const headers = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (typeof value === 'string') headers[key] = value; + else if (Array.isArray(value)) headers[key] = value.join(', '); + } + const response = await fetch(url, { + method: req.method, + headers, + body: ['GET', 'HEAD'].includes(req.method) ? undefined : req, + duplex: ['GET', 'HEAD'].includes(req.method) ? undefined : 'half', + redirect: 'manual', + }); + res.status(response.status); + response.headers.forEach((value, key) => { res.setHeader(key, value); }); + const body = await response.arrayBuffer(); + res.send(Buffer.from(body)); + }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); + } + + start(); + `, + ) + .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); + }); + } + }); +}); diff --git a/integration/tests/hono/machine.test.ts b/integration/tests/hono/machine.test.ts new file mode 100644 index 00000000000..03222730b57 --- /dev/null +++ b/integration/tests/hono/machine.test.ts @@ -0,0 +1,370 @@ +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('Hono 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.hono.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { getRequestListener } from '@hono/node-server'; + import { clerkMiddleware, getAuth } from '@clerk/hono'; + import express from 'express'; + import { Hono } from 'hono'; + import ViteExpress from 'vite-express'; + + const app = new Hono(); + app.use('*', clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); + + app.get('/me', (c) => { + const { userId, tokenType } = getAuth(c, { acceptsToken: 'api_key' }); + if (!userId) { + return c.json({ error: 'Unauthorized' }, 401); + } + return c.json({ userId, tokenType }); + }); + + const expressApp = express(); + const honoRequestListener = getRequestListener(app.fetch); + expressApp.use('/api', async (req, res) => { await honoRequestListener(req, res); }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); + `, + ) + .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.hono.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { getRequestListener } from '@hono/node-server'; + import { clerkMiddleware, getAuth } from '@clerk/hono'; + import express from 'express'; + import { Hono } from 'hono'; + import ViteExpress from 'vite-express'; + + const app = new Hono(); + app.use('*', clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); + + app.get('/m2m', (c) => { + const { subject, tokenType, isAuthenticated } = getAuth(c, { acceptsToken: 'm2m_token' }); + if (!isAuthenticated) { + return c.json({ error: 'Unauthorized' }, 401); + } + return c.json({ subject, tokenType }); + }); + + const expressApp = express(); + const honoRequestListener = getRequestListener(app.fetch); + expressApp.use('/api', async (req, res) => { await honoRequestListener(req, res); }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); + `, + ) + .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.hono.vite + .clone() + .addFile( + 'src/server/main.ts', + () => ` + import 'dotenv/config'; + import { getRequestListener } from '@hono/node-server'; + import { clerkMiddleware, getAuth } from '@clerk/hono'; + import express from 'express'; + import { Hono } from 'hono'; + import ViteExpress from 'vite-express'; + + const app = new Hono(); + app.use('*', clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); + + app.get('/oauth-verify', (c) => { + const { userId, tokenType } = getAuth(c, { acceptsToken: 'oauth_token' }); + if (!userId) { + return c.json({ error: 'Unauthorized' }, 401); + } + return c.json({ userId, tokenType }); + }); + + app.get('/oauth/callback', (c) => { + return c.json({ message: 'OAuth callback received' }); + }); + + const expressApp = express(); + const honoRequestListener = getRequestListener(app.fetch); + expressApp.use('/api', async (req, res) => { await honoRequestListener(req, res); }); + + const port = parseInt(process.env.PORT) || 3002; + ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); + `, + ) + .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); + }); + } + }); +}); From 5029a2e700531bdd96f54ace96ec22864f1777b7 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 24 Mar 2026 07:07:32 -0700 Subject: [PATCH 2/2] test(e2e): remove hono machine auth tests from this branch --- integration/tests/hono/machine.test.ts | 370 ------------------------- 1 file changed, 370 deletions(-) delete mode 100644 integration/tests/hono/machine.test.ts diff --git a/integration/tests/hono/machine.test.ts b/integration/tests/hono/machine.test.ts deleted file mode 100644 index 03222730b57..00000000000 --- a/integration/tests/hono/machine.test.ts +++ /dev/null @@ -1,370 +0,0 @@ -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('Hono 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.hono.vite - .clone() - .addFile( - 'src/server/main.ts', - () => ` - import 'dotenv/config'; - import { getRequestListener } from '@hono/node-server'; - import { clerkMiddleware, getAuth } from '@clerk/hono'; - import express from 'express'; - import { Hono } from 'hono'; - import ViteExpress from 'vite-express'; - - const app = new Hono(); - app.use('*', clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); - - app.get('/me', (c) => { - const { userId, tokenType } = getAuth(c, { acceptsToken: 'api_key' }); - if (!userId) { - return c.json({ error: 'Unauthorized' }, 401); - } - return c.json({ userId, tokenType }); - }); - - const expressApp = express(); - const honoRequestListener = getRequestListener(app.fetch); - expressApp.use('/api', async (req, res) => { await honoRequestListener(req, res); }); - - const port = parseInt(process.env.PORT) || 3002; - ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); - `, - ) - .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.hono.vite - .clone() - .addFile( - 'src/server/main.ts', - () => ` - import 'dotenv/config'; - import { getRequestListener } from '@hono/node-server'; - import { clerkMiddleware, getAuth } from '@clerk/hono'; - import express from 'express'; - import { Hono } from 'hono'; - import ViteExpress from 'vite-express'; - - const app = new Hono(); - app.use('*', clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); - - app.get('/m2m', (c) => { - const { subject, tokenType, isAuthenticated } = getAuth(c, { acceptsToken: 'm2m_token' }); - if (!isAuthenticated) { - return c.json({ error: 'Unauthorized' }, 401); - } - return c.json({ subject, tokenType }); - }); - - const expressApp = express(); - const honoRequestListener = getRequestListener(app.fetch); - expressApp.use('/api', async (req, res) => { await honoRequestListener(req, res); }); - - const port = parseInt(process.env.PORT) || 3002; - ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); - `, - ) - .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.hono.vite - .clone() - .addFile( - 'src/server/main.ts', - () => ` - import 'dotenv/config'; - import { getRequestListener } from '@hono/node-server'; - import { clerkMiddleware, getAuth } from '@clerk/hono'; - import express from 'express'; - import { Hono } from 'hono'; - import ViteExpress from 'vite-express'; - - const app = new Hono(); - app.use('*', clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY })); - - app.get('/oauth-verify', (c) => { - const { userId, tokenType } = getAuth(c, { acceptsToken: 'oauth_token' }); - if (!userId) { - return c.json({ error: 'Unauthorized' }, 401); - } - return c.json({ userId, tokenType }); - }); - - app.get('/oauth/callback', (c) => { - return c.json({ message: 'OAuth callback received' }); - }); - - const expressApp = express(); - const honoRequestListener = getRequestListener(app.fetch); - expressApp.use('/api', async (req, res) => { await honoRequestListener(req, res); }); - - const port = parseInt(process.env.PORT) || 3002; - ViteExpress.listen(expressApp, port, () => console.log('Server is listening on port ' + port)); - `, - ) - .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); - }); - } - }); -});