Skip to content

feat(backend): add experimental OAuth token exchange for OAuth applications - #10762

Draft
wyattjoh wants to merge 15 commits into
mainfrom
wyattjoh/backend-oauth-token-exchange
Draft

wyattjoh wants to merge 15 commits into
mainfrom
wyattjoh/backend-oauth-token-exchange

Conversation

@wyattjoh

@wyattjoh wyattjoh commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds clerkClient.oauthApplications.__experimental_exchangeToken(), which performs an OAuth 2.0 Token Exchange (RFC 8693) for an OAuth application and returns the issued token. Failures throw a new OAuthTokenExchangeError, exported from @clerk/backend/errors.

Why

@clerk/backend can verify OAuth access tokens and read the act claim of an exchanged token, but has no way to perform the exchange. Backend services that need to call a downstream API on a user's behalf have to hand-roll the form-encoded token request, client authentication, and error parsing.

How

  • The method loads the OAuth application through the Backend API and posts to its tokenFetchUrl, so callers pass only the application ID, client secret, and exchange parameters; no publishable key or discovery is needed. The trade-off is one extra Backend API request per exchange.
  • The request body is form-encoded with the urn:ietf:params:oauth:grant-type:token-exchange grant (RFC 8693 §2.1); audience and resource repeat when given arrays and scope is space-delimited.
  • Confidential clients authenticate with HTTP Basic, form-urlencoding the client ID and secret before base64 as RFC 6749 §2.3.1 requires (this differs from encodeURIComponent). Public clients omit clientSecret and send client_id in the body instead (RFC 6749 §3.2.1).
  • Token-endpoint errors (RFC 6749 §5.2) surface as OAuthTokenExchangeError, a ClerkError like MachineTokenVerificationError, with the OAuth error in code (aliased as error), errorDescription, and status; non-JSON or malformed responses use invalid_response. actorTokenType without actorToken is rejected before any request.
  • The method is a new member of an existing API, so it carries the __experimental_ prefix per docs/CONTRIBUTING.md, plus an @experimental JSDoc.

Before merge

  • Confirm the Frontend API token endpoint accepts the token-exchange grant; tests mock the endpoint.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@vercel

vercel Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Nov 11, 2026 7:34am UTC
swingset Ready Ready Preview Nov 11, 2026 7:34am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@changeset-bot

changeset-bot Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d340537

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

This PR includes changesets to release 11 packages
Name Type
@clerk/backend Minor
@clerk/astro Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/swingset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@10762

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@10762

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@10762

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@10762

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@10762

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@10762

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@10762

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@10762

@clerk/expo-biometrics

npm i https://pkg.pr.new/@clerk/expo-biometrics@10762

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@10762

@clerk/expo-native-components

npm i https://pkg.pr.new/@clerk/expo-native-components@10762

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@10762

@clerk/express

npm i https://pkg.pr.new/@clerk/express@10762

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@10762

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@10762

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@10762

@clerk/mosaic

npm i https://pkg.pr.new/@clerk/mosaic@10762

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@10762

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@10762

@clerk/react

npm i https://pkg.pr.new/@clerk/react@10762

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@10762

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@10762

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@10762

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@10762

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@10762

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@10762

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@10762

commit: d340537

… what Clerk supports

Drop subjectTokenType, requestedTokenType, actorToken, actorTokenType, and audience, and require a single resource. The SDK always sends the access-token subject_token_type that RFC 8693 section 2.1 requires.
…exchange

Clerk only permits confidential OAuth applications to perform token exchange, so drop the public-client branch that sent client_id in the body.
…publishable key

Take clientId and publishableKey (plus optional proxyUrl) per call and derive the Frontend API token endpoint from them, instead of loading the OAuth application through the Backend API. The exchange no longer requires the instance secret key.
Treat only HTTP 200 as a successful token-exchange response and require
scope, expires_in and refresh_token to be absent or correctly typed, so
malformed responses surface as OAuthTokenExchangeError invalid_response
instead of a raw TypeError or wrongly typed result.
Reject a resource with a fragment, a non-absolute URI, or a scheme other
than https or loopback http with an invalid_request OAuthTokenExchangeError
before any network request (RFC 8693 section 2.1). The caller's original
string is still sent unchanged.
Check the raw resource string against the RFC 3986 character set and
pct-encoding before the WHATWG parse, so strings WHATWG repairs (spaces,
tabs, backslashes, non-ASCII, bad escapes) are rejected instead of sent.
Also add an invalid_response error factory, rename the invalid_request
factory, drop a code comment and a cast, and share test helpers.

This branch was successfully deployed

2 active deployments
Preview – swingset — d340537a Deployed Nov 11, 2026 by vercel[bot]
Preview – clerk-js-sandbox — d340537a Deployed Nov 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant