Skip to content

feat(oauth): add private_key_jwt client authentication (RFC 7523)#8836

Open
gustavovalverde wants to merge 1 commit intomainfrom
feat/private-key-jwt
Open

feat(oauth): add private_key_jwt client authentication (RFC 7523)#8836
gustavovalverde wants to merge 1 commit intomainfrom
feat/private-key-jwt

Conversation

@gustavovalverde
Copy link
Copy Markdown
Contributor

@gustavovalverde gustavovalverde commented Mar 30, 2026

Summary

End-to-end private_key_jwt client authentication per RFC 7523, covering both sides of the OAuth exchange: server-side assertion verification in @better-auth/oauth-provider, and client-side assertion signing in the core OAuth2 primitives, SSO plugin, and generic OAuth plugin.

Closes #5935
Closes #6053, which targeted the legacy oidc-provider plugin and only implemented server-side verification.

What changed

Two new capabilities, each useless without the other:

Server-side verification (@better-auth/oauth-provider): the token, introspect, and revoke endpoints now accept client_assertion + client_assertion_type parameters. Clients registered with token_endpoint_auth_method: "private_key_jwt" provide their public keys via jwks or jwks_uri at registration; the server verifies assertion signatures against those keys, enforces jti single-use via the verification table, caps assertion lifetime, and rejects any attempt to fall back to secret-based auth.

Client-side signing (@better-auth/core, @better-auth/sso, generic-oauth): a signClientAssertion() utility constructs RFC 7523 JWTs. The SSO plugin resolves private keys at runtime via a resolvePrivateKey callback (supporting HSM/KMS without storing keys in the database) or inline via defaultSSO. Discovery now correctly selects private_key_jwt when the IdP requires it.

Security properties

  • JWKS URI fetch: HTTPS-only, private IP rejection, redirect blocked, 5-minute cache with stale fallback
  • JTI replay: tombstones stored until assertion exp; in-flight deduplication via process-local Set
  • Auth method enforcement: private_key_jwt clients cannot authenticate with client_secret
  • Assertion lifetime: exp required, capped by assertionMaxLifetime (default 5 min), advisory iat check when present
  • Auth material cleanup: switching auth methods clears the opposing credentials

Summary by cubic

Adds end-to-end private_key_jwt (RFC 7523) client authentication across the stack. Servers verify JWT client assertions; clients sign them for auth code, refresh, and client‑credentials flows.

  • New Features

    • Server (@better-auth/oauth-provider): Accept/verify client_assertion on token/introspect/revoke; register keys via jwks or HTTPS jwks_uri (mutually exclusive); enforce auth method; cap assertion lifetime via assertionMaxLifetime (default 5m); prevent jti replay; publish supported methods and endpoint‑specific signing algs in metadata; SSRF‑safe JWKS fetch with HTTPS‑only, private IP blocking, 5‑min cache, 5s timeout, and stale fallback; clear incompatible secrets/JWKS when switching methods; unified extractClientCredentials() for Basic/POST/private_key_jwt.
    • Client (@better-auth/core, @better-auth/sso, generic-oauth): signClientAssertion() utility; auth code, refresh, and client‑credentials requests support authentication: "private_key_jwt" with clientAssertion (JWK or PKCS#8 PEM, optional pre‑signed, expiresIn, aud from tokenEndpoint); SSO discovery selects private_key_jwt when the IdP requires it and signs via resolvePrivateKey or inline key (validated on update); SSO clientSecret is optional for private_key_jwt; new privateKeyId/privateKeyAlgorithm fields; generic-oauth auto‑passes tokenEndpoint and assertion across flows. Also fixes base64 encoding in client‑credentials token requests.
  • Migration

    • Provider: register clients with token_endpoint_auth_method: "private_key_jwt" and provide jwks or HTTPS jwks_uri. Switching auth methods auto‑clears incompatible credentials.
    • SSO: when discovery selects private_key_jwt, configure resolvePrivateKey (HSM/KMS supported) or provide an inline key; clientSecret becomes optional; set privateKeyId/privateKeyAlgorithm if needed. generic-oauth: set authentication: "private_key_jwt" and provide clientAssertion; the plugin auto‑passes tokenEndpoint for signing.

Written for commit 1a4c1d6. Summary will update on new commits.

Copilot AI review requested due to automatic review settings March 30, 2026 06:35
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
better-auth-demo Ignored Ignored Mar 30, 2026 7:37am
better-auth Skipped Skipped Mar 30, 2026 7:37am

Request Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 30, 2026

Open in StackBlitz

@better-auth/api-key

npm i https://pkg.pr.new/@better-auth/api-key@8836

better-auth

npm i https://pkg.pr.new/better-auth@8836

auth

npm i https://pkg.pr.new/auth@8836

@better-auth/core

npm i https://pkg.pr.new/@better-auth/core@8836

@better-auth/drizzle-adapter

npm i https://pkg.pr.new/@better-auth/drizzle-adapter@8836

@better-auth/electron

npm i https://pkg.pr.new/@better-auth/electron@8836

@better-auth/expo

npm i https://pkg.pr.new/@better-auth/expo@8836

@better-auth/i18n

npm i https://pkg.pr.new/@better-auth/i18n@8836

@better-auth/kysely-adapter

npm i https://pkg.pr.new/@better-auth/kysely-adapter@8836

@better-auth/memory-adapter

npm i https://pkg.pr.new/@better-auth/memory-adapter@8836

@better-auth/mongo-adapter

npm i https://pkg.pr.new/@better-auth/mongo-adapter@8836

@better-auth/oauth-provider

npm i https://pkg.pr.new/@better-auth/oauth-provider@8836

@better-auth/passkey

npm i https://pkg.pr.new/@better-auth/passkey@8836

@better-auth/prisma-adapter

npm i https://pkg.pr.new/@better-auth/prisma-adapter@8836

@better-auth/redis-storage

npm i https://pkg.pr.new/@better-auth/redis-storage@8836

@better-auth/scim

npm i https://pkg.pr.new/@better-auth/scim@8836

@better-auth/sso

npm i https://pkg.pr.new/@better-auth/sso@8836

@better-auth/stripe

npm i https://pkg.pr.new/@better-auth/stripe@8836

@better-auth/telemetry

npm i https://pkg.pr.new/@better-auth/telemetry@8836

@better-auth/test-utils

npm i https://pkg.pr.new/@better-auth/test-utils@8836

commit: 1a4c1d6

@gustavovalverde gustavovalverde changed the title feat(oauth): add private_key_jwt client authentication (RFC 7523) feat(oauth): add private_key_jwt client authentication (RFC 7523) Mar 30, 2026
@vercel vercel bot temporarily deployed to Preview – better-auth March 30, 2026 06:40 Inactive
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements end-to-end OAuth2/OIDC private_key_jwt client authentication (RFC 7523), adding server-side assertion verification in @better-auth/oauth-provider and client-side assertion signing across core OAuth2 helpers, SSO, and generic OAuth.

Changes:

  • Add private_key_jwt support to OAuth provider endpoints (token/introspect/revoke) including JWKS/JWKS URI registration, assertion verification, and JTI replay prevention.
  • Add client assertion signing utilities and wire private_key_jwt into core OAuth2 flows plus SSO/generic-oauth integrations.
  • Expand schemas, types, tests, and documentation to cover configuration and behavior.

Reviewed changes

Copilot reviewed 38 out of 38 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/sso/src/types.ts Extends SSO/OIDC config types to support private_key_jwt and runtime private key resolution.
packages/sso/src/routes/sso.ts Validates provider registration/update requirements and signs client assertions during token exchange.
packages/sso/src/routes/schemas.ts Adds route schema fields for private_key_jwt config (key id/alg).
packages/sso/src/routes/providers.ts Merges and validates updated OIDC configs including private_key_jwt requirements.
packages/sso/src/oidc/types.ts Extends hydrated discovery config typing to include private_key_jwt.
packages/sso/src/oidc/discovery.ts Updates discovery selection logic to return private_key_jwt when advertised/required.
packages/sso/src/oidc/discovery.test.ts Adds/updates test cases for private_key_jwt selection behavior.
packages/sso/src/oidc.test.ts Adds integration test covering SSO token exchange using private_key_jwt.
packages/oauth-provider/src/utils/index.ts Adds credential extraction with private_key_jwt, and supports pre-verified clients in credential validation.
packages/oauth-provider/src/utils/client-assertion.ts Introduces server-side JWT client assertion verification and JWKS/JWKS URI fetching with cache/SSRF protections.
packages/oauth-provider/src/types/oauth.ts Extends auth method + client metadata to include private_key_jwt and JWKS structures.
packages/oauth-provider/src/types/index.ts Adds assertionMaxLifetime option and stores JWKS/JWKS URI on schema clients.
packages/oauth-provider/src/token.ts Uses unified credential extraction and supports pre-verified (assertion-authenticated) clients.
packages/oauth-provider/src/schema.ts Adds persistence fields for jwks and jwksUri.
packages/oauth-provider/src/revoke.ts Adds private_key_jwt support to revocation endpoint via credential extraction.
packages/oauth-provider/src/register.ts Validates registration rules for private_key_jwt (jwks/jwks_uri, HTTPS/trusted origin) and stores JWKS material.
packages/oauth-provider/src/private-key-jwt.test.ts Adds comprehensive integration tests for assertion auth, replay prevention, jwks_uri, and registration validation.
packages/oauth-provider/src/private-key-jwt-e2e.test.ts Adds end-to-end test exercising RP↔provider flow using private_key_jwt.
packages/oauth-provider/src/oauthClient/index.ts Extends client create schemas to accept private_key_jwt + jwks/jwks_uri.
packages/oauth-provider/src/oauthClient/endpoints.ts Clears obsolete auth material when switching auth methods (secret vs JWKS).
packages/oauth-provider/src/oauthClient/endpoints.test.ts Adds CRUD tests for private_key_jwt clients and secret rotation restriction.
packages/oauth-provider/src/oauth.ts Updates endpoint schemas to accept client_assertion + client_assertion_type and registration metadata.
packages/oauth-provider/src/metadata.ts Advertises private_key_jwt methods and signing alg support in server metadata.
packages/oauth-provider/src/metadata.test.ts Updates metadata tests to include private_key_jwt.
packages/oauth-provider/src/introspect.ts Adds private_key_jwt support to introspection endpoint via credential extraction.
packages/core/src/oauth2/validate-authorization-code.ts Adds private_key_jwt support for auth-code exchange by signing/sending assertions.
packages/core/src/oauth2/refresh-access-token.ts Adds private_key_jwt support for refresh token flow by signing/sending assertions.
packages/core/src/oauth2/private-key-jwt-authentication.test.ts Adds tests ensuring assertions use the request token endpoint as audience across flows.
packages/core/src/oauth2/index.ts Exposes client assertion signing utilities/types from core OAuth2 module.
packages/core/src/oauth2/client-credentials-token.ts Adds private_key_jwt support for client-credentials flow by signing/sending assertions.
packages/core/src/oauth2/client-assertion.ts Implements signClientAssertion() to produce RFC 7523 JWT assertions.
packages/core/src/oauth2/client-assertion.test.ts Adds unit tests for assertion signing behavior and header/claim correctness.
packages/better-auth/src/plugins/generic-oauth/types.ts Extends generic-oauth config typing to allow private_key_jwt authentication.
packages/better-auth/src/plugins/generic-oauth/routes.ts Wires assertion config into callback token exchange for private_key_jwt.
packages/better-auth/src/plugins/generic-oauth/index.ts Wires assertion config into refresh flow for private_key_jwt.
docs/content/docs/plugins/sso.mdx Documents SSO private_key_jwt configuration, discovery behavior, and key resolution options.
docs/content/docs/plugins/oauth-provider.mdx Documents OAuth provider client auth methods and private_key_jwt registration/exchange requirements.
docs/content/docs/plugins/generic-oauth.mdx Documents generic OAuth private_key_jwt and client assertion configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel vercel bot temporarily deployed to Preview – better-auth March 30, 2026 06:49 Inactive
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 38 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/content/docs/plugins/oauth-provider.mdx">

<violation number="1" location="docs/content/docs/plugins/oauth-provider.mdx:502">
P3: Documentation now says `jwks`/`jwks_uri` are both unsupported and required for `private_key_jwt`. Update/remove the earlier “not yet supported” note to match the new support statement.</violation>
</file>

<file name="packages/oauth-provider/src/oauth.ts">

<violation number="1" location="packages/oauth-provider/src/oauth.ts:604">
P2: OpenAPI request schemas were not updated to include the newly accepted `client_assertion` and `client_assertion_type` fields for token/introspect/revoke endpoints.</violation>

<violation number="2" location="packages/oauth-provider/src/oauth.ts:1156">
P3: Registration response OpenAPI enum is stale: it still omits `private_key_jwt` even though the endpoint now accepts and can return it.</violation>
</file>

<file name="packages/core/src/oauth2/client-assertion.ts">

<violation number="1" location="packages/core/src/oauth2/client-assertion.ts:33">
P1: The `algorithm` parameter should be restricted to asymmetric algorithms only. Accepting any string allows insecure values like `"HS256"` or `"none"`, which are incompatible with `private_key_jwt` authentication that requires public key verification.

(Based on your team's feedback about restricting JWK alg to asymmetric algorithms only.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/oauth-provider/src/oauthClient/endpoints.test.ts">

<violation number="1" location="packages/oauth-provider/src/oauthClient/endpoints.test.ts:360">
P2: The status assertion is too broad: it also passes on 5xx server errors, which can hide endpoint regressions.</violation>
</file>

<file name="packages/oauth-provider/src/utils/client-assertion.ts">

<violation number="1" location="packages/oauth-provider/src/utils/client-assertion.ts:59">
P2: IPv6 unique local address check is incomplete. The `fc00::/7` range includes both `fc00::/8` and `fd00::/8`, but only `[fd` prefixed addresses are blocked. Add `[fc` to the check for complete SSRF protection.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 38 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel vercel bot temporarily deployed to Preview – better-auth March 30, 2026 07:16 Inactive
@vercel vercel bot temporarily deployed to Preview – better-auth March 30, 2026 07:20 Inactive
@vercel vercel bot temporarily deployed to Preview – better-auth March 30, 2026 07:28 Inactive
@vercel vercel bot temporarily deployed to Preview – better-auth March 30, 2026 07:32 Inactive
Add end-to-end support for private_key_jwt client authentication
across the OAuth provider (server-side verification), SSO plugin
(client-side signing against external IdPs), and generic OAuth plugin.

Server-side (@better-auth/oauth-provider):
- Accept private_key_jwt assertions at token, introspect, and revoke endpoints
- JWKS registration via jwks or jwks_uri (mutually exclusive, HTTPS enforced)
- JTI replay prevention via verification table with tombstones until exp
- Assertion lifetime cap (assertionMaxLifetime, default 5 minutes)
- Auth method enforcement: private_key_jwt clients cannot fall back to secrets
- JWKS URI caching (5-min TTL, 5s timeout, stale fallback)
- SSRF protection for jwks_uri (private IP blocking, redirect rejection)
- Unified extractClientCredentials() replacing 5-way duplication
- Obsolete auth material cleared on auth-method switch

Client-side (@better-auth/core, @better-auth/sso, generic-oauth):
- signClientAssertion() utility for RFC 7523 JWT construction
- resolvePrivateKey callback for HSM/KMS key resolution (no keys in DB)
- defaultSSO inline privateKey support for static configurations
- Discovery auto-selects private_key_jwt when IdP requires it
- clientSecret optional for private_key_jwt SSO providers
- Pre-signed assertion passthrough (skip signing for HSM/KMS workflows)
- Configurable expiresIn on client assertions

Also fixes pre-existing base64Url bug in client-credentials-token.ts.

Closes #5935
Supersedes #6053
@vercel vercel bot temporarily deployed to Preview – better-auth March 30, 2026 07:37 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for client assertions (private_key_jwt)

4 participants