diff --git a/.changeset/bright-spoons-fix.md b/.changeset/bright-spoons-fix.md new file mode 100644 index 00000000000..3592102363f --- /dev/null +++ b/.changeset/bright-spoons-fix.md @@ -0,0 +1,12 @@ +--- +'@clerk/backend': minor +--- + +Adds the ability to grab an instance's JWKS to the Backend API client. + +```ts + import { createClerkClient } from '@clerk/backend'; + + const clerkClient = createClerkClient(...); + await clerkClient.jwks.getJWKS(); +``` \ No newline at end of file diff --git a/packages/backend/src/api/__tests__/factory.test.ts b/packages/backend/src/api/__tests__/factory.test.ts index f722c890f94..7ffb78b43cc 100644 --- a/packages/backend/src/api/__tests__/factory.test.ts +++ b/packages/backend/src/api/__tests__/factory.test.ts @@ -1,6 +1,7 @@ import { http, HttpResponse } from 'msw'; import { describe, expect, it } from 'vitest'; +import jwksJson from '../../fixtures/jwks.json'; import userJson from '../../fixtures/user.json'; import { server, validateHeaders } from '../../mock-server'; import { createBackendApiClient } from '../factory'; @@ -275,4 +276,30 @@ describe('api.client', () => { expect(data[0].token).toBe(''); expect(data[0].scopes).toEqual(['email', 'profile']); }); + + describe('JWKS', () => { + it('executes a successful backend API request for a single resource and returns the raw response', async () => { + server.use( + http.get( + `https://api.clerk.test/v1/jwks`, + validateHeaders(() => { + return HttpResponse.json(jwksJson); + }), + ), + ); + + const response = await apiClient.jwks.getJwks(); + const key = response.keys?.[0]; + + expect(key).toBeDefined(); + expect(key?.kid).toBe('ins_1234'); + expect(key?.alg).toBe('RS256'); + expect(key?.kty).toBe('RSA'); + expect(key?.use).toBe('sig'); + expect(key?.e).toBe('BQGF'); + expect(key?.n).toBe( + 'xV3jihnMy4sr5jJ4S66YTc6FxnFsVy3weiyJFYOAdo515AZMrpMMdraAiVmnXZfolZpv7CcnsnG290cg-XfGRNk-Jil_tJt2SLGtiT9LtWT_iev4zN8veRGzTaOb6C-Qb6T_8xsjP_sp0a92zyNgyc4UxR-acMmOqxjkHmx1q0U1fCom83WI59Yu5VmvLM4MA-1sLkmAE1bTzp4ie-_xu9anwsS3H97MONGtildB4nAG0L-lj7tReNHoYLkciEKCqqUMoK-o6JN29OKozpqiI4dVv0oityWw2ygf6eR5qrKZZjrjbAMt_emXBFGQ5Y1QSsriJoRoykGcdbXaU7S_QV', + ); + }); + }); }); diff --git a/packages/backend/src/api/endpoints/JwksApi.ts b/packages/backend/src/api/endpoints/JwksApi.ts new file mode 100644 index 00000000000..f8aef648efc --- /dev/null +++ b/packages/backend/src/api/endpoints/JwksApi.ts @@ -0,0 +1,13 @@ +import type { JwksJSON } from '../resources/JSON'; +import { AbstractAPI } from './AbstractApi'; + +const basePath = '/jwks'; + +export class JwksAPI extends AbstractAPI { + public async getJwks() { + return this.request({ + method: 'GET', + path: basePath, + }); + } +} diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index f1ea10ac9ce..c7e94d4e073 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -5,6 +5,7 @@ export * from './ClientApi'; export * from './DomainApi'; export * from './EmailAddressApi'; export * from './InvitationApi'; +export * from './JwksApi'; export * from './OrganizationApi'; export * from './PhoneNumberApi'; export * from './RedirectUrlApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index 47098188dbe..068ba90fb9b 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -5,6 +5,7 @@ import { DomainAPI, EmailAddressAPI, InvitationAPI, + JwksAPI, OrganizationAPI, PhoneNumberAPI, RedirectUrlAPI, @@ -31,6 +32,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { clients: new ClientAPI(request), emailAddresses: new EmailAddressAPI(request), invitations: new InvitationAPI(request), + jwks: new JwksAPI(request), organizations: new OrganizationAPI(request), phoneNumbers: new PhoneNumberAPI(request), redirectUrls: new RedirectUrlAPI(request), diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 5ad5f0cc078..9d8b20fbcb6 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -126,6 +126,19 @@ export interface ExternalAccountJSON extends ClerkResourceJSON { verification: VerificationJSON | null; } +export interface JwksJSON { + keys?: JwksKeyJSON[]; +} + +export interface JwksKeyJSON { + use: string; + kty: string; + kid: string; + alg: string; + n: string; + e: string; +} + export interface SamlAccountJSON extends ClerkResourceJSON { object: typeof ObjectType.SamlAccount; provider: string; diff --git a/packages/backend/src/fixtures/jwks.json b/packages/backend/src/fixtures/jwks.json new file mode 100644 index 00000000000..14150fd33b1 --- /dev/null +++ b/packages/backend/src/fixtures/jwks.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "ins_1234", + "alg": "RS256", + "n": "xV3jihnMy4sr5jJ4S66YTc6FxnFsVy3weiyJFYOAdo515AZMrpMMdraAiVmnXZfolZpv7CcnsnG290cg-XfGRNk-Jil_tJt2SLGtiT9LtWT_iev4zN8veRGzTaOb6C-Qb6T_8xsjP_sp0a92zyNgyc4UxR-acMmOqxjkHmx1q0U1fCom83WI59Yu5VmvLM4MA-1sLkmAE1bTzp4ie-_xu9anwsS3H97MONGtildB4nAG0L-lj7tReNHoYLkciEKCqqUMoK-o6JN29OKozpqiI4dVv0oityWw2ygf6eR5qrKZZjrjbAMt_emXBFGQ5Y1QSsriJoRoykGcdbXaU7S_QV", + "e": "BQGF" + } + ] +}