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/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts index 4c61f35d235..eb5a70d3b62 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 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( + 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/factory.ts b/packages/backend/src/api/factory.ts index ce83dac4328..5283aafbc09 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -68,6 +68,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { 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/JSON.ts b/packages/backend/src/api/resources/JSON.ts index fc72bcbf817..e7f7aaec564 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -701,6 +701,7 @@ export interface SamlAccountConnectionJSON extends ClerkResourceJSON { 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/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/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');