Repository: clerk/javascript
Pull request #5588: feat(backend): Add JWKS BAPI endpoint
Author: login: tmilewski
Labels: name: backend
Created: 2025-04-10T17:38:12Z | Merged: 2025-04-10T22:38:30Z

## Description

Adds the JWKS BAPI endpoint.

**Note:** It does not return an object key. As such, it returns the (typed) raw JSON response.

<!-- 
  Thanks for contributing to Clerk. Make sure to read the contributing guide at https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md before opening a PR!

  **Please create a feature request before starting work on any significant change.**

  Write a brief description of the changes introduced in this PR.
  Include screenshots/videos if they help convey the change.

  Also explain how one can test the change.
-->

<!-- Fixes #(issue number) -->

Fixes ECO-584

## Checklist

- [x] `pnpm test` runs as expected.
- [x] `pnpm build` runs as expected.
- [ ] (If applicable) [JSDoc comments](https://jsdoc.app/about-getting-started.html) have been added or updated for any package exports
- [ ] (If applicable) [Documentation](https://github.com/clerk/clerk-docs) has been updated

## Type of change

- [ ] 🐛 Bug fix
- [x] 🌟 New feature
- [ ] 🔨 Breaking change
- [ ] 📖 Refactoring / dependency upgrade / documentation
- [ ] other:



Diff:
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('<token>');
     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<JwksJSON>({
+      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"
+    }
+  ]
+}


Reviews:
user: login: dstaley
body: lgtm, just need to make sure the changeset is correct
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T22:07:15Z

Review comments:
diff_hunk: @@ -0,0 +1,12 @@
+---
+'@clerk/backend': patch
path: .changeset/bright-spoons-fix.md
user: login: dstaley
body: since this is adding functionality shouldn't it be a minor?
created_at: 2025-04-10T22:06:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,12 @@
+---
+'@clerk/backend': patch
+---
+
+Adds the ability to verify proxy checks to the Backend API client.
path: .changeset/bright-spoons-fix.md
user: login: dstaley
body: I think this is copy pasted from another PR?
created_at: 2025-04-10T22:06:41Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-10T17:38:16Z
body: ###  🦋  Changeset detected

Latest commit: 3b819f3269fb98cebfa944aa6f596a1c1bbdb6a4

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 11 packages</summary>

  | Name                        | Type  |
| --------------------------- | ----- |
| @clerk/backend              | Minor |
| @clerk/agent-toolkit        | Patch |
| @clerk/astro                | Patch |
| @clerk/express              | Patch |
| @clerk/fastify              | Patch |
| @clerk/nextjs               | Patch |
| @clerk/nuxt                 | Patch |
| @clerk/react-router         | Patch |
| @clerk/remix                | Patch |
| @clerk/tanstack-react-start | Patch |
| @clerk/testing              | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/clerk/javascript/new/tm/add-jwks?filename=.changeset/three-trees-jam.md&value=---%0A%22%40clerk%2Fbackend%22%3A%20patch%0A---%0A%0Afeat(backend)%3A%20Add%20JWKS%20BAPI%20endpoint%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-10T17:38:17Z
body: [vc]: #KLD5kJMxjtm1rH0LtxP6NNEmVYXdy+/bwwxNHRgz3W8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjbGVyay1qcy1zYW5kYm94IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImNsZXJrLWpzLXNhbmRib3gtZ2l0LXRtLWFkZC1qd2tzLmNsZXJrc3RhZ2UuZGV2In0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9jbGVyay1wcm9kdWN0aW9uL2NsZXJrLWpzLXNhbmRib3gvOTZIMkN6aW1UUkdLd2k0UW5ER1QxZnpidW5nbyIsInByZXZpZXdVcmwiOiJjbGVyay1qcy1zYW5kYm94LWdpdC10bS1hZGQtandrcy5jbGVya3N0YWdlLmRldiIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9jbGVyay1qcyJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **clerk-js-sandbox** | ✅ Ready ([Inspect](https://vercel.com/clerk-production/clerk-js-sandbox/96H2CzimTRGKwi4QnDGT1fzbungo)) | [Visit Preview](https://clerk-js-sandbox-git-tm-add-jwks.clerkstage.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/clerk-js-sandbox-git-tm-add-jwks.clerkstage.dev?via=pr-comment-feedback-link) | Apr 10, 2025 10:29pm |


author_association: NONE