diff --git a/.changeset/hot-tables-worry.md b/.changeset/hot-tables-worry.md new file mode 100644 index 00000000000..2637253987a --- /dev/null +++ b/.changeset/hot-tables-worry.md @@ -0,0 +1,5 @@ +--- +"@clerk/backend": minor +--- + +WIP M2M Tokens diff --git a/packages/backend/src/api/endpoints/MachineApi.ts b/packages/backend/src/api/endpoints/MachineApi.ts new file mode 100644 index 00000000000..fafdc4868f7 --- /dev/null +++ b/packages/backend/src/api/endpoints/MachineApi.ts @@ -0,0 +1,65 @@ +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 GetMachineListParams = { + limit?: number; + offset?: number; + query?: string; +}; + +export class MachineApi extends AbstractAPI { + async list(queryParams: GetMachineListParams = {}) { + return this.request>({ + method: 'GET', + path: basePath, + queryParams, + }); + } + + async create(bodyParams: CreateMachineParams) { + return this.request({ + method: 'POST', + path: basePath, + bodyParams, + }); + } + + async update(params: UpdateMachineParams) { + const { machineId, ...bodyParams } = params; + this.requireId(machineId); + return this.request({ + method: 'PATCH', + path: joinPaths(basePath, machineId), + bodyParams, + }); + } + + async delete(machineId: string) { + this.requireId(machineId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, machineId), + }); + } + + async get(machineId: string) { + this.requireId(machineId); + return this.request({ + method: 'GET', + path: joinPaths(basePath, machineId), + }); + } +} diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 4c61f35d235..9b6faa28ab8 100644 --- a/packages/backend/src/api/endpoints/MachineTokensApi.ts +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -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 & { machineSecret?: string | null }; + +type CreateMachineTokenParams = WithMachineSecret<{ + name: string; + subject: string; + claims?: Record | null; + scopes?: string[]; + createdBy?: string | null; + secondsUntilExpiration?: number | null; +}>; + +type UpdateMachineTokenParams = WithMachineSecret< + { + m2mTokenId: string; + revoked?: boolean; + } & Pick +>; + +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({ - method: 'POST', - path: joinPaths(basePath, 'verify'), - bodyParams: { secret }, - }); + /** + * Overrides the instance secret with a 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( + this.#withMachineSecretHeader( + { + method: 'POST', + path: basePath, + bodyParams, + }, + machineSecret, + ), + ); + } + + async update(params: UpdateMachineTokenParams) { + const { m2mTokenId, machineSecret, ...bodyParams } = params; + this.requireId(m2mTokenId); + return this.request( + 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( + this.#withMachineSecretHeader( + { + method: 'POST', + path: joinPaths(basePath, m2mTokenId, 'revoke'), + bodyParams, + }, + machineSecret, + ), + ); + } + + async verifySecret(params: VerifyMachineTokenParams) { + const { secret, machineSecret } = params; + return this.request( + this.#withMachineSecretHeader( + { + method: 'POST', + path: joinPaths(basePath, 'verify'), + bodyParams: { secret }, + }, + machineSecret, + ), + ); } } diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index 26b3f2e8d3f..e7eeb312c68 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -11,6 +11,7 @@ export * from './EmailAddressApi'; export * from './IdPOAuthAccessTokenApi'; export * from './InstanceApi'; export * from './InvitationApi'; +export * from './MachineApi'; export * from './MachineTokensApi'; export * from './JwksApi'; export * from './JwtTemplatesApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index ce83dac4328..b2817cc10ba 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -13,6 +13,7 @@ import { InvitationAPI, JwksAPI, JwtTemplatesApi, + MachineApi, MachineTokensApi, OAuthApplicationsApi, OrganizationAPI, @@ -64,10 +65,12 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { invitations: new InvitationAPI(request), jwks: new JwksAPI(request), jwtTemplates: new JwtTemplatesApi(request), + machines: new MachineApi(request), machineTokens: new MachineTokensApi( buildRequest({ ...options, skipApiVersionInUrl: true, + requireSecretKey: false, }), ), oauthApplications: new OAuthApplicationsApi(request), diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index b86475dab60..29c5d1b3dcb 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -104,12 +104,12 @@ export function buildRequest(options: BuildRequestOptions) { // Build headers const headers = new Headers({ 'Clerk-API-Version': SUPPORTED_BAPI_VERSION, - 'User-Agent': userAgent, + [constants.Headers.UserAgent]: userAgent, ...headerParams, }); - if (secretKey) { - headers.set('Authorization', `Bearer ${secretKey}`); + if (secretKey && !headers.has(constants.Headers.Authorization)) { + headers.set(constants.Headers.Authorization, `Bearer ${secretKey}`); } let res: Response | undefined; diff --git a/packages/backend/src/api/resources/Deserializer.ts b/packages/backend/src/api/resources/Deserializer.ts index 4f47da13e50..2db6e993609 100644 --- a/packages/backend/src/api/resources/Deserializer.ts +++ b/packages/backend/src/api/resources/Deserializer.ts @@ -15,6 +15,7 @@ import { InstanceSettings, Invitation, JwtTemplate, + Machine, MachineToken, OauthAccessToken, OAuthApplication, @@ -132,6 +133,8 @@ function jsonToObject(item: any): any { return Invitation.fromJSON(item); case ObjectType.JwtTemplate: return JwtTemplate.fromJSON(item); + case ObjectType.Machine: + return Machine.fromJSON(item); case ObjectType.MachineToken: return MachineToken.fromJSON(item); case ObjectType.OauthAccessToken: diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index fc72bcbf817..682a4ffee42 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -34,6 +34,7 @@ export const ObjectType = { InstanceRestrictions: 'instance_restrictions', InstanceSettings: 'instance_settings', Invitation: 'invitation', + Machine: 'machine', MachineToken: 'machine_to_machine_token', JwtTemplate: 'jwt_template', OauthAccessToken: 'oauth_access_token', @@ -698,9 +699,19 @@ export interface SamlAccountConnectionJSON extends ClerkResourceJSON { updated_at: number; } +export interface MachineJSON extends ClerkResourceJSON { + object: typeof ObjectType.Machine; + id: string; + name: string; + instance_id: string; + created_at: number; + updated_at: number; +} + export interface MachineTokenJSON extends ClerkResourceJSON { object: typeof ObjectType.MachineToken; name: string; + secret?: string; subject: string; scopes: string[]; claims: Record | null; diff --git a/packages/backend/src/api/resources/Machine.ts b/packages/backend/src/api/resources/Machine.ts new file mode 100644 index 00000000000..16b2f9b010f --- /dev/null +++ b/packages/backend/src/api/resources/Machine.ts @@ -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); + } +} diff --git a/packages/backend/src/api/resources/MachineToken.ts b/packages/backend/src/api/resources/MachineToken.ts index 1d19837bcdf..3b9340c09dc 100644 --- a/packages/backend/src/api/resources/MachineToken.ts +++ b/packages/backend/src/api/resources/MachineToken.ts @@ -15,9 +15,10 @@ export class MachineToken { readonly creationReason: string | null, readonly createdAt: number, readonly updatedAt: number, + readonly secret?: string, ) {} - static fromJSON(data: MachineTokenJSON) { + static fromJSON(data: MachineTokenJSON): MachineToken { return new MachineToken( data.id, data.name, @@ -32,6 +33,7 @@ export class MachineToken { data.creation_reason, data.created_at, data.updated_at, + data.secret, ); } } diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 1353e249ab7..034ab10fd3e 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -30,6 +30,7 @@ export * from './InstanceRestrictions'; export * from './InstanceSettings'; export * from './Invitation'; export * from './JSON'; +export * from './Machine'; export * from './MachineToken'; export * from './JwtTemplate'; export * from './OauthAccessToken'; diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index ad76138290b..79cc31e9176 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -206,7 +206,7 @@ async function verifyMachineToken( ): Promise> { try { const client = createBackendApiClient(options); - const verifiedToken = await client.machineTokens.verifySecret(secret); + const verifiedToken = await client.machineTokens.verifySecret({ secret }); return { data: verifiedToken, tokenType: TokenType.MachineToken, errors: undefined }; } catch (err: any) { return handleClerkAPIError(TokenType.MachineToken, err, 'Machine token not found');