Skip to content

Commit b033fb4

Browse files
committed
test(e2e): add machine auth tests for Express, Fastify, and Hono
1 parent d976a82 commit b033fb4

3 files changed

Lines changed: 1164 additions & 0 deletions

File tree

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import type { User } from '@clerk/backend';
2+
import { createClerkClient } from '@clerk/backend';
3+
import { TokenType } from '@clerk/backend/internal';
4+
import { expect, test } from '@playwright/test';
5+
6+
import type { Application } from '../../models/application';
7+
import { appConfigs } from '../../presets';
8+
import { instanceKeys } from '../../presets/envs';
9+
import type { FakeAPIKey, FakeMachineNetwork, FakeOAuthApp, FakeUser } from '../../testUtils';
10+
import {
11+
createFakeMachineNetwork,
12+
createFakeOAuthApp,
13+
createJwtM2MToken,
14+
createTestUtils,
15+
obtainOAuthAccessToken,
16+
} from '../../testUtils';
17+
18+
test.describe('Express machine authentication @machine', () => {
19+
test.describe('API key auth', () => {
20+
test.describe.configure({ mode: 'parallel' });
21+
let app: Application;
22+
let fakeUser: FakeUser;
23+
let fakeBapiUser: User;
24+
let fakeAPIKey: FakeAPIKey;
25+
26+
test.beforeAll(async () => {
27+
test.setTimeout(120_000);
28+
29+
app = await appConfigs.express.vite
30+
.clone()
31+
.addFile(
32+
'src/server/main.ts',
33+
() => `
34+
import 'dotenv/config';
35+
import { clerkMiddleware, getAuth } from '@clerk/express';
36+
import express from 'express';
37+
import ViteExpress from 'vite-express';
38+
39+
const app = express();
40+
app.use(clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY }));
41+
42+
app.get('/api/me', (req, res) => {
43+
const { userId, tokenType } = getAuth(req, { acceptsToken: 'api_key' });
44+
if (!userId) {
45+
return res.status(401).json({ error: 'Unauthorized' });
46+
}
47+
return res.json({ userId, tokenType });
48+
});
49+
50+
const port = parseInt(process.env.PORT) || 3002;
51+
ViteExpress.listen(app, port, () => console.log('Server is listening on port ' + port));
52+
`,
53+
)
54+
.commit();
55+
56+
await app.setup();
57+
await app.withEnv(appConfigs.envs.withAPIKeys);
58+
await app.dev();
59+
60+
const u = createTestUtils({ app });
61+
fakeUser = u.services.users.createFakeUser();
62+
fakeBapiUser = await u.services.users.createBapiUser(fakeUser);
63+
fakeAPIKey = await u.services.users.createFakeAPIKey(fakeBapiUser.id);
64+
});
65+
66+
test.afterAll(async () => {
67+
await fakeAPIKey.revoke();
68+
await fakeUser.deleteIfExists();
69+
await app.teardown();
70+
});
71+
72+
test('should return 401 if no API key is provided', async ({ request }) => {
73+
const url = new URL('/api/me', app.serverUrl);
74+
const res = await request.get(url.toString());
75+
expect(res.status()).toBe(401);
76+
});
77+
78+
test('should return 401 if API key is invalid', async ({ request }) => {
79+
const url = new URL('/api/me', app.serverUrl);
80+
const res = await request.get(url.toString(), {
81+
headers: { Authorization: 'Bearer invalid_key' },
82+
});
83+
expect(res.status()).toBe(401);
84+
});
85+
86+
test('should return 200 with auth object if API key is valid', async ({ request }) => {
87+
const url = new URL('/api/me', app.serverUrl);
88+
const res = await request.get(url.toString(), {
89+
headers: {
90+
Authorization: `Bearer ${fakeAPIKey.secret}`,
91+
},
92+
});
93+
const apiKeyData = await res.json();
94+
expect(res.status()).toBe(200);
95+
expect(apiKeyData.userId).toBe(fakeBapiUser.id);
96+
expect(apiKeyData.tokenType).toBe(TokenType.ApiKey);
97+
});
98+
99+
for (const [tokenType, token] of [
100+
['M2M', 'mt_test_mismatch'],
101+
['OAuth', 'oat_test_mismatch'],
102+
] as const) {
103+
test(`rejects ${tokenType} token on API key route (token type mismatch)`, async ({ request }) => {
104+
const url = new URL('/api/me', app.serverUrl);
105+
const res = await request.get(url.toString(), {
106+
headers: { Authorization: `Bearer ${token}` },
107+
});
108+
expect(res.status()).toBe(401);
109+
});
110+
}
111+
});
112+
113+
test.describe('M2M auth', () => {
114+
test.describe.configure({ mode: 'parallel' });
115+
let app: Application;
116+
let network: FakeMachineNetwork;
117+
118+
test.beforeAll(async () => {
119+
test.setTimeout(120_000);
120+
121+
const client = createClerkClient({
122+
secretKey: instanceKeys.get('with-api-keys').sk,
123+
});
124+
network = await createFakeMachineNetwork(client);
125+
126+
app = await appConfigs.express.vite
127+
.clone()
128+
.addFile(
129+
'src/server/main.ts',
130+
() => `
131+
import 'dotenv/config';
132+
import { clerkMiddleware, getAuth } from '@clerk/express';
133+
import express from 'express';
134+
import ViteExpress from 'vite-express';
135+
136+
const app = express();
137+
app.use(clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY }));
138+
139+
app.get('/api/m2m', (req, res) => {
140+
const { subject, tokenType, isAuthenticated } = getAuth(req, { acceptsToken: 'm2m_token' });
141+
if (!isAuthenticated) {
142+
return res.status(401).json({ error: 'Unauthorized' });
143+
}
144+
return res.json({ subject, tokenType });
145+
});
146+
147+
const port = parseInt(process.env.PORT) || 3002;
148+
ViteExpress.listen(app, port, () => console.log('Server is listening on port ' + port));
149+
`,
150+
)
151+
.commit();
152+
153+
await app.setup();
154+
155+
const env = appConfigs.envs.withAPIKeys
156+
.clone()
157+
.setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', network.primaryServer.secretKey);
158+
await app.withEnv(env);
159+
await app.dev();
160+
});
161+
162+
test.afterAll(async () => {
163+
await network.cleanup();
164+
await app.teardown();
165+
});
166+
167+
test('rejects requests with invalid M2M tokens', async ({ request }) => {
168+
const res = await request.get(app.serverUrl + '/api/m2m');
169+
expect(res.status()).toBe(401);
170+
171+
const res2 = await request.get(app.serverUrl + '/api/m2m', {
172+
headers: { Authorization: 'Bearer mt_xxx' },
173+
});
174+
expect(res2.status()).toBe(401);
175+
});
176+
177+
test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ request }) => {
178+
const res = await request.get(app.serverUrl + '/api/m2m', {
179+
headers: { Authorization: `Bearer ${network.unscopedSenderToken.token}` },
180+
});
181+
expect(res.status()).toBe(401);
182+
});
183+
184+
test('authorizes M2M requests when sender machine has proper access', async ({ page, context }) => {
185+
const u = createTestUtils({ app, page, context });
186+
187+
const res = await u.page.request.get(app.serverUrl + '/api/m2m', {
188+
headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` },
189+
});
190+
expect(res.status()).toBe(200);
191+
const body = await res.json();
192+
expect(body.subject).toBe(network.scopedSender.id);
193+
expect(body.tokenType).toBe(TokenType.M2MToken);
194+
});
195+
196+
test('authorizes after dynamically granting scope', async ({ page, context }) => {
197+
const u = createTestUtils({ app, page, context });
198+
199+
await u.services.clerk.machines.createScope(network.unscopedSender.id, network.primaryServer.id);
200+
const m2mToken = await u.services.clerk.m2m.createToken({
201+
machineSecretKey: network.unscopedSender.secretKey,
202+
secondsUntilExpiration: 60 * 30,
203+
});
204+
205+
const res = await u.page.request.get(app.serverUrl + '/api/m2m', {
206+
headers: { Authorization: `Bearer ${m2mToken.token}` },
207+
});
208+
expect(res.status()).toBe(200);
209+
const body = await res.json();
210+
expect(body.subject).toBe(network.unscopedSender.id);
211+
expect(body.tokenType).toBe(TokenType.M2MToken);
212+
await u.services.clerk.m2m.revokeToken({ m2mTokenId: m2mToken.id });
213+
});
214+
215+
test('verifies JWT format M2M token via local verification', async ({ request }) => {
216+
const client = createClerkClient({
217+
secretKey: instanceKeys.get('with-api-keys').sk,
218+
});
219+
const jwtToken = await createJwtM2MToken(client, network.scopedSender.secretKey);
220+
221+
const res = await request.get(app.serverUrl + '/api/m2m', {
222+
headers: { Authorization: `Bearer ${jwtToken.token}` },
223+
});
224+
expect(res.status()).toBe(200);
225+
const body = await res.json();
226+
expect(body.subject).toBe(network.scopedSender.id);
227+
expect(body.tokenType).toBe(TokenType.M2MToken);
228+
});
229+
230+
for (const [tokenType, token] of [
231+
['API key', 'ak_test_mismatch'],
232+
['OAuth', 'oat_test_mismatch'],
233+
] as const) {
234+
test(`rejects ${tokenType} token on M2M route (token type mismatch)`, async ({ request }) => {
235+
const res = await request.get(app.serverUrl + '/api/m2m', {
236+
headers: { Authorization: `Bearer ${token}` },
237+
});
238+
expect(res.status()).toBe(401);
239+
});
240+
}
241+
});
242+
243+
test.describe('OAuth auth', () => {
244+
test.describe.configure({ mode: 'parallel' });
245+
let app: Application;
246+
let fakeUser: FakeUser;
247+
let fakeOAuth: FakeOAuthApp;
248+
249+
test.beforeAll(async () => {
250+
test.setTimeout(120_000);
251+
252+
app = await appConfigs.express.vite
253+
.clone()
254+
.addFile(
255+
'src/server/main.ts',
256+
() => `
257+
import 'dotenv/config';
258+
import { clerkMiddleware, getAuth } from '@clerk/express';
259+
import express from 'express';
260+
import ViteExpress from 'vite-express';
261+
262+
const app = express();
263+
app.use(clerkMiddleware({ publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY }));
264+
265+
app.get('/api/oauth-verify', (req, res) => {
266+
const { userId, tokenType } = getAuth(req, { acceptsToken: 'oauth_token' });
267+
if (!userId) {
268+
return res.status(401).json({ error: 'Unauthorized' });
269+
}
270+
return res.json({ userId, tokenType });
271+
});
272+
273+
app.get('/api/oauth/callback', (req, res) => {
274+
return res.json({ message: 'OAuth callback received' });
275+
});
276+
277+
const port = parseInt(process.env.PORT) || 3002;
278+
ViteExpress.listen(app, port, () => console.log('Server is listening on port ' + port));
279+
`,
280+
)
281+
.commit();
282+
283+
await app.setup();
284+
await app.withEnv(appConfigs.envs.withAPIKeys);
285+
await app.dev();
286+
287+
const u = createTestUtils({ app });
288+
fakeUser = u.services.users.createFakeUser();
289+
await u.services.users.createBapiUser(fakeUser);
290+
291+
const clerkClient = createClerkClient({
292+
secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'),
293+
publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'),
294+
});
295+
296+
fakeOAuth = await createFakeOAuthApp(clerkClient, `${app.serverUrl}/api/oauth/callback`);
297+
});
298+
299+
test.afterAll(async () => {
300+
await fakeOAuth.cleanup();
301+
await fakeUser.deleteIfExists();
302+
await app.teardown();
303+
});
304+
305+
test('verifies valid OAuth access token obtained through authorization flow', async ({ page, context }) => {
306+
const u = createTestUtils({ app, page, context });
307+
308+
const accessToken = await obtainOAuthAccessToken({
309+
page: u.page,
310+
oAuthApp: fakeOAuth.oAuthApp,
311+
redirectUri: `${app.serverUrl}/api/oauth/callback`,
312+
fakeUser,
313+
signIn: u.po.signIn,
314+
});
315+
316+
const res = await u.page.request.get(new URL('/api/oauth-verify', app.serverUrl).toString(), {
317+
headers: { Authorization: `Bearer ${accessToken}` },
318+
});
319+
expect(res.status()).toBe(200);
320+
const authData = await res.json();
321+
expect(authData.userId).toBeDefined();
322+
expect(authData.tokenType).toBe(TokenType.OAuthToken);
323+
});
324+
325+
test('rejects request without OAuth token', async ({ request }) => {
326+
const url = new URL('/api/oauth-verify', app.serverUrl);
327+
const res = await request.get(url.toString());
328+
expect(res.status()).toBe(401);
329+
});
330+
331+
test('rejects request with invalid OAuth token', async ({ request }) => {
332+
const url = new URL('/api/oauth-verify', app.serverUrl);
333+
const res = await request.get(url.toString(), {
334+
headers: { Authorization: 'Bearer invalid_oauth_token' },
335+
});
336+
expect(res.status()).toBe(401);
337+
});
338+
339+
for (const [tokenType, token] of [
340+
['API key', 'ak_test_mismatch'],
341+
['M2M', 'mt_test_mismatch'],
342+
] as const) {
343+
test(`rejects ${tokenType} token on OAuth route (token type mismatch)`, async ({ request }) => {
344+
const url = new URL('/api/oauth-verify', app.serverUrl);
345+
const res = await request.get(url.toString(), {
346+
headers: { Authorization: `Bearer ${token}` },
347+
});
348+
expect(res.status()).toBe(401);
349+
});
350+
}
351+
});
352+
});

0 commit comments

Comments
 (0)