|
| 1 | +import { createClerkClient, type M2MToken, type Machine } from '@clerk/backend'; |
| 2 | +import { faker } from '@faker-js/faker'; |
| 3 | +import { expect, test } from '@playwright/test'; |
| 4 | + |
| 5 | +import type { Application } from '../../models/application'; |
| 6 | +import { appConfigs } from '../../presets'; |
| 7 | +import { instanceKeys } from '../../presets/envs'; |
| 8 | +import { createTestUtils } from '../../testUtils'; |
| 9 | + |
| 10 | +test.describe('machine-to-machine auth @machine', () => { |
| 11 | + test.describe.configure({ mode: 'parallel' }); |
| 12 | + let app: Application; |
| 13 | + let primaryApiServer: Machine; |
| 14 | + let emailServer: Machine; |
| 15 | + let analyticsServer: Machine; |
| 16 | + let emailServerM2MToken: M2MToken; |
| 17 | + let analyticsServerM2MToken: M2MToken; |
| 18 | + |
| 19 | + test.beforeAll(async () => { |
| 20 | + const fakeCompanyName = faker.company.name(); |
| 21 | + |
| 22 | + // Create primary machine using instance secret key |
| 23 | + const client = createClerkClient({ |
| 24 | + secretKey: instanceKeys.get('with-api-keys').sk, |
| 25 | + }); |
| 26 | + primaryApiServer = await client.machines.create({ |
| 27 | + name: `${fakeCompanyName} Primary API Server`, |
| 28 | + }); |
| 29 | + |
| 30 | + app = await appConfigs.express.vite |
| 31 | + .clone() |
| 32 | + .addFile( |
| 33 | + 'src/server/main.ts', |
| 34 | + () => ` |
| 35 | + import 'dotenv/config'; |
| 36 | + import { clerkClient } from '@clerk/express'; |
| 37 | + import express from 'express'; |
| 38 | + import ViteExpress from 'vite-express'; |
| 39 | +
|
| 40 | + const app = express(); |
| 41 | +
|
| 42 | + app.get('/api/protected', async (req, res) => { |
| 43 | + const secret = req.get('Authorization')?.split(' ')[1]; |
| 44 | + |
| 45 | + try { |
| 46 | + const m2mToken = await clerkClient.m2mTokens.verifySecret({ secret }); |
| 47 | + res.send('Protected response ' + m2mToken.id); |
| 48 | + } catch { |
| 49 | + res.status(401).send('Unauthorized'); |
| 50 | + } |
| 51 | + }); |
| 52 | +
|
| 53 | + const port = parseInt(process.env.PORT as string) || 3002; |
| 54 | + ViteExpress.listen(app, port, () => console.log('Server started')); |
| 55 | + `, |
| 56 | + ) |
| 57 | + .commit(); |
| 58 | + |
| 59 | + await app.setup(); |
| 60 | + |
| 61 | + // Using the created machine, set a machine secret key using the primary machine's secret key |
| 62 | + const env = appConfigs.envs.withAPIKeys |
| 63 | + .clone() |
| 64 | + .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', primaryApiServer.secretKey); |
| 65 | + await app.withEnv(env); |
| 66 | + await app.dev(); |
| 67 | + |
| 68 | + // Email server can access primary API server |
| 69 | + emailServer = await client.machines.create({ |
| 70 | + name: `${fakeCompanyName} Email Server`, |
| 71 | + scopedMachines: [primaryApiServer.id], |
| 72 | + }); |
| 73 | + emailServerM2MToken = await client.m2mTokens.create({ |
| 74 | + machineSecretKey: emailServer.secretKey, |
| 75 | + secondsUntilExpiration: 60 * 30, |
| 76 | + }); |
| 77 | + |
| 78 | + // Analytics server cannot access primary API server |
| 79 | + analyticsServer = await client.machines.create({ |
| 80 | + name: `${fakeCompanyName} Analytics Server`, |
| 81 | + // No scoped machines |
| 82 | + }); |
| 83 | + analyticsServerM2MToken = await client.m2mTokens.create({ |
| 84 | + machineSecretKey: analyticsServer.secretKey, |
| 85 | + secondsUntilExpiration: 60 * 30, |
| 86 | + }); |
| 87 | + }); |
| 88 | + |
| 89 | + test.afterAll(async () => { |
| 90 | + const client = createClerkClient({ |
| 91 | + secretKey: instanceKeys.get('with-api-keys').sk, |
| 92 | + }); |
| 93 | + |
| 94 | + await client.m2mTokens.revoke({ |
| 95 | + m2mTokenId: emailServerM2MToken.id, |
| 96 | + }); |
| 97 | + await client.m2mTokens.revoke({ |
| 98 | + m2mTokenId: analyticsServerM2MToken.id, |
| 99 | + }); |
| 100 | + await client.machines.delete(emailServer.id); |
| 101 | + await client.machines.delete(primaryApiServer.id); |
| 102 | + await client.machines.delete(analyticsServer.id); |
| 103 | + |
| 104 | + await app.teardown(); |
| 105 | + }); |
| 106 | + |
| 107 | + test('rejects requests with invalid M2M tokens', async ({ page, context }) => { |
| 108 | + const u = createTestUtils({ app, page, context }); |
| 109 | + |
| 110 | + const res = await u.page.request.get(app.serverUrl + '/api/protected', { |
| 111 | + headers: { |
| 112 | + Authorization: `Bearer invalid`, |
| 113 | + }, |
| 114 | + }); |
| 115 | + expect(res.status()).toBe(401); |
| 116 | + expect(await res.text()).toBe('Unauthorized'); |
| 117 | + |
| 118 | + const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { |
| 119 | + headers: { |
| 120 | + Authorization: `Bearer mt_xxx`, |
| 121 | + }, |
| 122 | + }); |
| 123 | + expect(res2.status()).toBe(401); |
| 124 | + expect(await res2.text()).toBe('Unauthorized'); |
| 125 | + }); |
| 126 | + |
| 127 | + test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ page, context }) => { |
| 128 | + const u = createTestUtils({ app, page, context }); |
| 129 | + |
| 130 | + const res = await u.page.request.get(app.serverUrl + '/api/protected', { |
| 131 | + headers: { |
| 132 | + Authorization: `Bearer ${analyticsServerM2MToken.secret}`, |
| 133 | + }, |
| 134 | + }); |
| 135 | + expect(res.status()).toBe(401); |
| 136 | + expect(await res.text()).toBe('Unauthorized'); |
| 137 | + }); |
| 138 | + |
| 139 | + test('authorizes M2M requests when sender machine has proper access to receiver machine', async ({ |
| 140 | + page, |
| 141 | + context, |
| 142 | + }) => { |
| 143 | + const u = createTestUtils({ app, page, context }); |
| 144 | + |
| 145 | + // Email server can access primary API server |
| 146 | + const res = await u.page.request.get(app.serverUrl + '/api/protected', { |
| 147 | + headers: { |
| 148 | + Authorization: `Bearer ${emailServerM2MToken.secret}`, |
| 149 | + }, |
| 150 | + }); |
| 151 | + expect(res.status()).toBe(200); |
| 152 | + expect(await res.text()).toBe('Protected response ' + emailServerM2MToken.id); |
| 153 | + |
| 154 | + // Analytics server can access primary API server after adding scope |
| 155 | + await u.services.clerk.machines.createScope(analyticsServer.id, primaryApiServer.id); |
| 156 | + const m2mToken = await u.services.clerk.m2mTokens.create({ |
| 157 | + machineSecretKey: analyticsServer.secretKey, |
| 158 | + secondsUntilExpiration: 60 * 30, |
| 159 | + }); |
| 160 | + |
| 161 | + const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { |
| 162 | + headers: { |
| 163 | + Authorization: `Bearer ${m2mToken.secret}`, |
| 164 | + }, |
| 165 | + }); |
| 166 | + expect(res2.status()).toBe(200); |
| 167 | + expect(await res2.text()).toBe('Protected response ' + m2mToken.id); |
| 168 | + await u.services.clerk.m2mTokens.revoke({ |
| 169 | + m2mTokenId: m2mToken.id, |
| 170 | + }); |
| 171 | + }); |
| 172 | +}); |
0 commit comments