-
Notifications
You must be signed in to change notification settings - Fork 372
chore(repo,backend): Add machine-to-machine integration tests #6500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a9787ef
dc8c7be
58191b1
1707c12
56867b2
757ea76
dc77a3f
ebff149
4f0bd61
ebd8b96
398c3cf
7fad6ca
48bc893
2fa0b44
98b79d7
91a7e56
c224d6f
b52c3b9
45a490d
268c92b
8807419
2dff52b
b628254
53a9eb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@clerk/backend": minor | ||
--- | ||
|
||
Exports `Machine` and `M2MToken` resource classes | ||
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -293,6 +293,7 @@ jobs: | |||||
'nuxt', | ||||||
'react-router', | ||||||
'billing', | ||||||
'machine' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙃
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So there's a follow up PR for this that will group all "machine" related tests (api keys, m2m) |
||||||
] | ||||||
wobsoriano marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
test-project: ['chrome'] | ||||||
include: | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { createClerkClient, type M2MToken, type Machine } from '@clerk/backend'; | ||
import { faker } from '@faker-js/faker'; | ||
import { expect, test } from '@playwright/test'; | ||
|
||
import type { Application } from '../../models/application'; | ||
import { appConfigs } from '../../presets'; | ||
import { instanceKeys } from '../../presets/envs'; | ||
import { createTestUtils } from '../../testUtils'; | ||
|
||
test.describe('machine-to-machine auth @machine', () => { | ||
test.describe.configure({ mode: 'parallel' }); | ||
let app: Application; | ||
let primaryApiServer: Machine; | ||
let emailServer: Machine; | ||
let analyticsServer: Machine; | ||
let emailServerM2MToken: M2MToken; | ||
let analyticsServerM2MToken: M2MToken; | ||
|
||
test.beforeAll(async () => { | ||
const fakeCompanyName = faker.company.name(); | ||
|
||
// Create primary machine using instance secret key | ||
const client = createClerkClient({ | ||
secretKey: instanceKeys.get('with-api-keys').sk, | ||
}); | ||
primaryApiServer = await client.machines.create({ | ||
name: `${fakeCompanyName} Primary API Server`, | ||
}); | ||
|
||
app = await appConfigs.express.vite | ||
.clone() | ||
.addFile( | ||
'src/server/main.ts', | ||
() => ` | ||
import 'dotenv/config'; | ||
import { clerkClient } from '@clerk/express'; | ||
import express from 'express'; | ||
import ViteExpress from 'vite-express'; | ||
|
||
const app = express(); | ||
|
||
app.get('/api/protected', async (req, res) => { | ||
const secret = req.get('Authorization')?.split(' ')[1]; | ||
|
||
try { | ||
const m2mToken = await clerkClient.m2mTokens.verifySecret({ secret }); | ||
res.send('Protected response ' + m2mToken.id); | ||
} catch { | ||
res.status(401).send('Unauthorized'); | ||
} | ||
}); | ||
|
||
const port = parseInt(process.env.PORT as string) || 3002; | ||
ViteExpress.listen(app, port, () => console.log('Server started')); | ||
`, | ||
) | ||
.commit(); | ||
|
||
await app.setup(); | ||
|
||
// Using the created machine, set a machine secret key using the primary machine's secret key | ||
const env = appConfigs.envs.withAPIKeys | ||
.clone() | ||
.setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', primaryApiServer.secretKey); | ||
await app.withEnv(env); | ||
await app.dev(); | ||
|
||
// Email server can access primary API server | ||
emailServer = await client.machines.create({ | ||
name: `${fakeCompanyName} Email Server`, | ||
scopedMachines: [primaryApiServer.id], | ||
}); | ||
emailServerM2MToken = await client.m2mTokens.create({ | ||
machineSecretKey: emailServer.secretKey, | ||
secondsUntilExpiration: 60 * 30, | ||
}); | ||
|
||
// Analytics server cannot access primary API server | ||
analyticsServer = await client.machines.create({ | ||
name: `${fakeCompanyName} Analytics Server`, | ||
// No scoped machines | ||
}); | ||
analyticsServerM2MToken = await client.m2mTokens.create({ | ||
machineSecretKey: analyticsServer.secretKey, | ||
secondsUntilExpiration: 60 * 30, | ||
}); | ||
}); | ||
|
||
test.afterAll(async () => { | ||
const client = createClerkClient({ | ||
secretKey: instanceKeys.get('with-api-keys').sk, | ||
}); | ||
|
||
await client.m2mTokens.revoke({ | ||
m2mTokenId: emailServerM2MToken.id, | ||
}); | ||
await client.m2mTokens.revoke({ | ||
m2mTokenId: analyticsServerM2MToken.id, | ||
}); | ||
await client.machines.delete(emailServer.id); | ||
await client.machines.delete(primaryApiServer.id); | ||
await client.machines.delete(analyticsServer.id); | ||
|
||
await app.teardown(); | ||
}); | ||
|
||
test('rejects requests with invalid M2M tokens', async ({ page, context }) => { | ||
const u = createTestUtils({ app, page, context }); | ||
|
||
const res = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
headers: { | ||
Authorization: `Bearer invalid`, | ||
}, | ||
}); | ||
expect(res.status()).toBe(401); | ||
expect(await res.text()).toBe('Unauthorized'); | ||
|
||
const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
headers: { | ||
Authorization: `Bearer mt_xxx`, | ||
}, | ||
}); | ||
expect(res2.status()).toBe(401); | ||
expect(await res2.text()).toBe('Unauthorized'); | ||
}); | ||
|
||
test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ page, context }) => { | ||
const u = createTestUtils({ app, page, context }); | ||
|
||
const res = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
headers: { | ||
Authorization: `Bearer ${analyticsServerM2MToken.secret}`, | ||
}, | ||
}); | ||
expect(res.status()).toBe(401); | ||
expect(await res.text()).toBe('Unauthorized'); | ||
}); | ||
|
||
test('authorizes M2M requests when sender machine has proper access to receiver machine', async ({ | ||
page, | ||
context, | ||
}) => { | ||
const u = createTestUtils({ app, page, context }); | ||
|
||
// Email server can access primary API server | ||
const res = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
headers: { | ||
Authorization: `Bearer ${emailServerM2MToken.secret}`, | ||
}, | ||
}); | ||
expect(res.status()).toBe(200); | ||
expect(await res.text()).toBe('Protected response ' + emailServerM2MToken.id); | ||
|
||
// Analytics server can access primary API server after adding scope | ||
await u.services.clerk.machines.createScope(analyticsServer.id, primaryApiServer.id); | ||
const m2mToken = await u.services.clerk.m2mTokens.create({ | ||
machineSecretKey: analyticsServer.secretKey, | ||
secondsUntilExpiration: 60 * 30, | ||
}); | ||
|
||
const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
headers: { | ||
Authorization: `Bearer ${m2mToken.secret}`, | ||
}, | ||
}); | ||
expect(res2.status()).toBe(200); | ||
expect(await res2.text()).toBe('Protected response ' + m2mToken.id); | ||
await u.services.clerk.m2mTokens.revoke({ | ||
m2mTokenId: m2mToken.id, | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -344,6 +344,18 @@ | |
"env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], | ||
"inputs": ["integration/**"], | ||
"outputLogs": "new-only" | ||
}, | ||
"//#test:integration:machine": { | ||
"dependsOn": [ | ||
"@clerk/testing#build", | ||
"@clerk/clerk-js#build", | ||
"@clerk/backend#build", | ||
"@clerk/nextjs#build", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are testing against nextjs, right ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to my comment above, the api keys test needs nextjs |
||
"@clerk/express#build" | ||
], | ||
"env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], | ||
"inputs": ["integration/**"], | ||
"outputLogs": "new-only" | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Clarify wording and verify “classes” vs “types”; double-check semver level
The body currently says “resource classes,” while the PR summary mentions exporting “types.” Ensure the note matches the actual exports (classes vs types). Also, adding new public exports is often considered a minor release per SemVer, not a patch—confirm if “patch” is intended.
If the intent is generic and accurate regardless of implementation detail, you can update the copy to avoid ambiguity:
Optionally, if this is a minor release:
Run this script to verify the nature of the exports and ensure the wording is accurate:
🏁 Script executed:
Length of output: 6239
Update semver to minor for new exports
The term “resource classes” is accurate—both
Machine
andM2MToken
are declared asexport class
inpackages/backend/src/api/resources
. However, exposing new public API requires a minor bump, not a patch.📝 Committable suggestion
🤖 Prompt for AI Agents