diff --git a/.changeset/ready-wasps-look.md b/.changeset/ready-wasps-look.md new file mode 100644 index 00000000000..0111a16e3de --- /dev/null +++ b/.changeset/ready-wasps-look.md @@ -0,0 +1,18 @@ +--- +'@clerk/backend': minor +--- + +Adds the ability to perform CRUD operations on JWT Templates to the Backend API client. + + +```ts + import { createClerkClient } from '@clerk/backend'; + + const clerkClient = createClerkClient(...); + + await clerkClient.jwtTemplates.list({...}); + await clerkClient.jwtTemplates.get('templateId'); + await clerkClient.jwtTemplates.create({...}); + await clerkClient.jwtTemplates.update({...}); + await clerkClient.jwtTemplates.delete('templateId'); +``` \ No newline at end of file diff --git a/packages/backend/src/api/endpoints/JwtTemplatesApi.ts b/packages/backend/src/api/endpoints/JwtTemplatesApi.ts new file mode 100644 index 00000000000..66e3dc72a0f --- /dev/null +++ b/packages/backend/src/api/endpoints/JwtTemplatesApi.ts @@ -0,0 +1,94 @@ +import type { ClerkPaginationRequest } from '@clerk/types'; +import { joinPaths } from 'src/util/path'; + +import type { DeletedObject, JwtTemplate } from '../resources'; +import { AbstractAPI } from './AbstractApi'; + +const basePath = '/jwt_templates'; + +type Claims = object; + +type CreateJWTTemplateParams = { + /** + * JWT template name + */ + name: string; + /** + * JWT template claims in JSON format + */ + claims: Claims; + /** + * JWT token lifetime + */ + lifetime?: number | null | undefined; + /** + * JWT token allowed clock skew + */ + allowedClockSkew?: number | null | undefined; + /** + * Whether a custom signing key/algorithm is also provided for this template + */ + customSigningKey?: boolean | undefined; + /** + * The custom signing algorithm to use when minting JWTs. Required if `custom_signing_key` is `true`. + */ + signingAlgorithm?: string | null | undefined; + /** + * The custom signing private key to use when minting JWTs. Required if `custom_signing_key` is `true`. + */ + signingKey?: string | null | undefined; +}; + +type UpdateJWTTemplateParams = CreateJWTTemplateParams & { + /** + * JWT template ID + */ + templateId: string; +}; + +export class JwtTemplatesApi extends AbstractAPI { + public async list(params: ClerkPaginationRequest = {}) { + return this.request({ + method: 'GET', + path: basePath, + queryParams: { ...params, paginated: true }, + }); + } + + public async get(templateId: string) { + this.requireId(templateId); + + return this.request({ + method: 'GET', + path: joinPaths(basePath, templateId), + }); + } + + public async create(params: CreateJWTTemplateParams) { + return this.request({ + method: 'POST', + path: basePath, + bodyParams: params, + }); + } + + public async update(params: UpdateJWTTemplateParams) { + const { templateId, ...bodyParams } = params; + + this.requireId(templateId); + return this.request({ + method: 'PATCH', + path: joinPaths(basePath, templateId), + bodyParams, + }); + } + + public async delete(templateId: string) { + this.requireId(templateId); + + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, templateId), + }); + } +} diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index 9c9fa8b1745..90ca493b116 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -6,6 +6,7 @@ export * from './DomainApi'; export * from './EmailAddressApi'; export * from './InvitationApi'; export * from './JwksApi'; +export * from './JwtTemplatesApi'; export * from './OrganizationApi'; export * from './PhoneNumberApi'; export * from './ProxyCheckApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index 75f443e6a14..4e81685b813 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -6,6 +6,7 @@ import { EmailAddressAPI, InvitationAPI, JwksAPI, + JwtTemplatesApi, OrganizationAPI, PhoneNumberAPI, ProxyCheckAPI, @@ -34,6 +35,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { emailAddresses: new EmailAddressAPI(request), invitations: new InvitationAPI(request), jwks: new JwksAPI(request), + jwtTemplates: new JwtTemplatesApi(request), organizations: new OrganizationAPI(request), phoneNumbers: new PhoneNumberAPI(request), proxyChecks: new ProxyCheckAPI(request), diff --git a/packages/backend/src/api/resources/Deserializer.ts b/packages/backend/src/api/resources/Deserializer.ts index 8ed7fc730e1..dc7770e4de1 100644 --- a/packages/backend/src/api/resources/Deserializer.ts +++ b/packages/backend/src/api/resources/Deserializer.ts @@ -6,6 +6,7 @@ import { Email, EmailAddress, Invitation, + JwtTemplate, OauthAccessToken, Organization, OrganizationInvitation, @@ -82,6 +83,8 @@ function jsonToObject(item: any): any { return Email.fromJSON(item); case ObjectType.Invitation: return Invitation.fromJSON(item); + case ObjectType.JwtTemplate: + return JwtTemplate.fromJSON(item); case ObjectType.OauthAccessToken: return OauthAccessToken.fromJSON(item); case ObjectType.Organization: diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index b06ae457751..08201d86921 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -20,6 +20,7 @@ export const ObjectType = { FacebookAccount: 'facebook_account', GoogleAccount: 'google_account', Invitation: 'invitation', + JwtTemplate: 'jwt_template', OauthAccessToken: 'oauth_access_token', Organization: 'organization', OrganizationDomain: 'organization_domain', @@ -140,6 +141,19 @@ export interface JwksKeyJSON { e: string; } +export interface JwtTemplateJSON extends ClerkResourceJSON { + object: typeof ObjectType.JwtTemplate; + id: string; + name: string; + claims: object; + lifetime: number; + allowed_clock_skew: number; + custom_signing_key: boolean; + signing_algorithm: string; + created_at: number; + updated_at: number; +} + export interface SamlAccountJSON extends ClerkResourceJSON { object: typeof ObjectType.SamlAccount; provider: string; diff --git a/packages/backend/src/api/resources/JwtTemplate.ts b/packages/backend/src/api/resources/JwtTemplate.ts new file mode 100644 index 00000000000..ef53e5d21a7 --- /dev/null +++ b/packages/backend/src/api/resources/JwtTemplate.ts @@ -0,0 +1,29 @@ +import type { JwtTemplateJSON } from './JSON'; + +export class JwtTemplate { + constructor( + readonly id: string, + readonly name: string, + readonly claims: object, + readonly lifetime: number, + readonly allowedClockSkew: number, + readonly customSigningKey: boolean, + readonly signingAlgorithm: string, + readonly createdAt: number, + readonly updatedAt: number, + ) {} + + static fromJSON(data: JwtTemplateJSON): JwtTemplate { + return new JwtTemplate( + data.id, + data.name, + data.claims, + data.lifetime, + data.allowed_clock_skew, + data.custom_signing_key, + data.signing_algorithm, + data.created_at, + data.updated_at, + ); + } +} diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index efaa43dfbb6..3398e59303f 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -20,6 +20,7 @@ export * from './ExternalAccount'; export * from './IdentificationLink'; export * from './Invitation'; export * from './JSON'; +export * from './JwtTemplate'; export * from './OauthAccessToken'; export * from './Organization'; export * from './OrganizationInvitation'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 786f0d1c837..eda48e7e90a 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -64,6 +64,7 @@ export type { ExternalAccountJSON, IdentificationLinkJSON, InvitationJSON, + JwtTemplateJSON, OauthAccessTokenJSON, OrganizationJSON, OrganizationDomainJSON, @@ -99,6 +100,7 @@ export type { EmailAddress, ExternalAccount, Invitation, + JwtTemplate, OauthAccessToken, Organization, OrganizationDomain,