-
Notifications
You must be signed in to change notification settings - Fork 376
feat(backend): Introduce M2M endpoints authentication using machine secret keys #6229
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 18 commits
05d6c8e
ca7a8be
af6a27b
fa94227
8dcd607
5d78030
7bb3eb8
424a5a4
1dbd41b
7c3063c
9dab708
db38ca5
5ce88ee
c33e3fd
f9526af
cb6c822
68bcb7e
e900e13
6c0fc64
c1d1ae2
d53115d
d87f937
7ff0538
e844565
017bb4b
e26660e
0f7387d
f78ddcc
1492a1e
a8c66e1
201cb23
64afde6
b38465b
18b76da
d91404c
37a3d65
1d69db8
b26bd76
d609285
2e080db
7055b8a
051dd85
7371a32
d96d436
6642321
239b6ba
a8d310f
2a74cf9
60b139a
ad9a0ec
8ca8009
7569d95
ad5a0f9
b72e891
2b2ce8e
afb5923
01653ae
e05d414
85e47f1
e2ede5f
c98d1ed
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 | ||
--- | ||
|
||
WIP M2M Tokens |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { joinPaths } from '../../util/path'; | ||
import type { PaginatedResourceResponse } from '../resources/Deserializer'; | ||
import type { Machine } from '../resources/Machine'; | ||
import { AbstractAPI } from './AbstractApi'; | ||
|
||
const basePath = '/machines'; | ||
|
||
type CreateMachineParams = { | ||
name: string; | ||
}; | ||
|
||
type UpdateMachineParams = { | ||
machineId: string; | ||
name: string; | ||
}; | ||
|
||
type DeleteMachineParams = { | ||
machineId: string; | ||
}; | ||
|
||
type GetMachineListParams = { | ||
limit?: number; | ||
offset?: number; | ||
query?: string; | ||
}; | ||
|
||
export class MachineApi extends AbstractAPI { | ||
async list(queryParams: GetMachineListParams = {}) { | ||
return this.request<PaginatedResourceResponse<Machine[]>>({ | ||
method: 'GET', | ||
path: basePath, | ||
queryParams, | ||
}); | ||
} | ||
|
||
async create(bodyParams: CreateMachineParams) { | ||
return this.request<Machine>({ | ||
method: 'POST', | ||
path: basePath, | ||
bodyParams, | ||
}); | ||
} | ||
|
||
async update(params: UpdateMachineParams) { | ||
const { machineId, ...bodyParams } = params; | ||
this.requireId(machineId); | ||
return this.request<Machine>({ | ||
method: 'PATCH', | ||
path: joinPaths(basePath, machineId), | ||
bodyParams, | ||
}); | ||
} | ||
|
||
async delete(params: DeleteMachineParams) { | ||
const { machineId } = params; | ||
this.requireId(machineId); | ||
return this.request<Machine>({ | ||
method: 'DELETE', | ||
path: joinPaths(basePath, machineId), | ||
}); | ||
} | ||
|
||
async get(machineId: string) { | ||
this.requireId(machineId); | ||
return this.request<Machine>({ | ||
method: 'GET', | ||
path: joinPaths(basePath, machineId), | ||
}); | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,111 @@ | ||
import { joinPaths } from '../../util/path'; | ||
import type { ClerkBackendApiRequestOptions } from '../request'; | ||
import type { MachineToken } from '../resources/MachineToken'; | ||
import { AbstractAPI } from './AbstractApi'; | ||
|
||
const basePath = '/m2m_tokens'; | ||
|
||
type WithMachineSecret<T> = T & { machineSecret?: string | null }; | ||
|
||
type CreateMachineTokenParams = WithMachineSecret<{ | ||
name: string; | ||
subject: string; | ||
claims?: Record<string, any> | null; | ||
scopes?: string[]; | ||
createdBy?: string | null; | ||
secondsUntilExpiration?: number | null; | ||
}>; | ||
|
||
type UpdateMachineTokenParams = WithMachineSecret< | ||
{ | ||
m2mTokenId: string; | ||
revoked?: boolean; | ||
} & Pick<CreateMachineTokenParams, 'secondsUntilExpiration' | 'claims' | 'scopes'> | ||
>; | ||
|
||
type RevokeMachineTokenParams = WithMachineSecret<{ | ||
m2mTokenId: string; | ||
revocationReason?: string | null; | ||
}>; | ||
|
||
type VerifyMachineTokenParams = WithMachineSecret<{ | ||
secret: string; | ||
}>; | ||
|
||
export class MachineTokensApi extends AbstractAPI { | ||
async verifySecret(secret: string) { | ||
return this.request<MachineToken>({ | ||
method: 'POST', | ||
path: joinPaths(basePath, 'verify'), | ||
bodyParams: { secret }, | ||
}); | ||
/** | ||
* Overrides the instance secret with the machine secret. | ||
*/ | ||
#withMachineSecretHeader( | ||
options: ClerkBackendApiRequestOptions, | ||
machineSecret?: string | null, | ||
): ClerkBackendApiRequestOptions { | ||
if (machineSecret) { | ||
return { | ||
...options, | ||
headerParams: { | ||
Authorization: `Bearer ${machineSecret}`, | ||
}, | ||
}; | ||
} | ||
return options; | ||
} | ||
|
||
async create(params: CreateMachineTokenParams) { | ||
const { machineSecret, ...bodyParams } = params; | ||
return this.request<MachineToken>( | ||
this.#withMachineSecretHeader( | ||
{ | ||
method: 'POST', | ||
path: basePath, | ||
bodyParams, | ||
}, | ||
machineSecret, | ||
), | ||
); | ||
} | ||
|
||
async update(params: UpdateMachineTokenParams) { | ||
const { m2mTokenId, machineSecret, ...bodyParams } = params; | ||
this.requireId(m2mTokenId); | ||
return this.request<MachineToken>( | ||
this.#withMachineSecretHeader( | ||
{ | ||
method: 'PATCH', | ||
path: joinPaths(basePath, m2mTokenId), | ||
bodyParams, | ||
}, | ||
machineSecret, | ||
), | ||
); | ||
} | ||
|
||
async revoke(params: RevokeMachineTokenParams) { | ||
const { m2mTokenId, machineSecret, ...bodyParams } = params; | ||
this.requireId(m2mTokenId); | ||
return this.request<MachineToken>( | ||
this.#withMachineSecretHeader( | ||
{ | ||
method: 'POST', | ||
path: joinPaths(basePath, m2mTokenId, 'revoke'), | ||
bodyParams, | ||
}, | ||
machineSecret, | ||
), | ||
); | ||
} | ||
|
||
async verifySecret(params: VerifyMachineTokenParams) { | ||
const { secret, machineSecret } = params; | ||
return this.request<MachineToken>( | ||
this.#withMachineSecretHeader( | ||
{ | ||
method: 'POST', | ||
path: joinPaths(basePath, 'verify'), | ||
bodyParams: { secret }, | ||
}, | ||
machineSecret, | ||
), | ||
); | ||
} | ||
wobsoriano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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. 💡 Verification agent ❓ Verification inconclusiveConsider adding tests for the refactored API methods. The significant refactoring of method signatures and addition of machine secret support would benefit from comprehensive test coverage to ensure the new parameter object pattern works correctly and machine secret authorization functions as expected. 🏁 Script executed: #!/bin/bash
# Search for existing tests for MachineTokensApi to verify test coverage
fd "MachineTokensApi" --type f --extension ts --extension js | grep -i test Length of output: 75 Let’s check for any references to 🏁 Script executed: #!/bin/bash
# Search for references to MachineTokensApi in all test files
rg --files-with-matches 'MachineTokensApi' -g '*.{test,spec}.{ts,tsx,js}'
rg --files-with-matches 'MachineTokensApi' -g '**/__tests__/**/*.{ts,tsx,js}' Length of output: 151 Add unit tests for MachineTokensApi methods The refactoring introduces new parameter patterns and machine-secret header logic that currently lack test coverage. Please add tests in • 🤖 Prompt for AI Agents
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { MachineJSON } from './JSON'; | ||
|
||
export class Machine { | ||
constructor( | ||
readonly id: string, | ||
readonly name: string, | ||
readonly instanceId: string, | ||
readonly createdAt: number, | ||
readonly updatedAt: number, | ||
) {} | ||
|
||
static fromJSON(data: MachineJSON): Machine { | ||
return new Machine(data.id, data.name, data.instance_id, data.created_at, data.updated_at); | ||
} | ||
} |
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
Consider adding comprehensive tests for the new MachineApi.
This new API client provides full CRUD operations for machines and would benefit from test coverage to ensure all operations work correctly, including parameter validation and proper HTTP method usage.
🏁 Script executed:
Length of output: 69
🏁 Script executed:
Length of output: 13112
Add unit tests for MachineApi
No existing tests reference the new MachineApi class. Per our testing requirements, please add a Jest/Vitest unit-test suite covering all CRUD methods:
• Create a test file, e.g.
packages/backend/src/api/endpoints/tests/MachineApi.test.ts
• Mock the underlying HTTP request handler and verify:
/machines
with proper query params/machines/:id
, and throws when machineId is missing/machines/:id
, and throws when machineId is missing/machines/:id
, and throws when id is missing• Assert correct typing of request and response payloads, and error handling
🤖 Prompt for AI Agents