feat(oauth): add private_key_jwt client authentication (RFC 7523)#8836
feat(oauth): add private_key_jwt client authentication (RFC 7523)#8836gustavovalverde wants to merge 1 commit intomainfrom
private_key_jwt client authentication (RFC 7523)#8836Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
@better-auth/api-key
better-auth
auth
@better-auth/core
@better-auth/drizzle-adapter
@better-auth/electron
@better-auth/expo
@better-auth/i18n
@better-auth/kysely-adapter
@better-auth/memory-adapter
@better-auth/mongo-adapter
@better-auth/oauth-provider
@better-auth/passkey
@better-auth/prisma-adapter
@better-auth/redis-storage
@better-auth/scim
@better-auth/sso
@better-auth/stripe
@better-auth/telemetry
@better-auth/test-utils
commit: |
private_key_jwt client authentication (RFC 7523)
d81967b to
34dea45
Compare
There was a problem hiding this comment.
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_jwtsupport 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_jwtinto 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.
34dea45 to
53e47ca
Compare
There was a problem hiding this comment.
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.
53e47ca to
7170564
Compare
There was a problem hiding this comment.
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.
7170564 to
36a14f8
Compare
36a14f8 to
4557643
Compare
4557643 to
64c67dd
Compare
64c67dd to
f03a57a
Compare
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
f03a57a to
1a4c1d6
Compare
Summary
End-to-end
private_key_jwtclient 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-providerplugin 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 acceptclient_assertion+client_assertion_typeparameters. Clients registered withtoken_endpoint_auth_method: "private_key_jwt"provide their public keys viajwksorjwks_uriat registration; the server verifies assertion signatures against those keys, enforcesjtisingle-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): asignClientAssertion()utility constructs RFC 7523 JWTs. The SSO plugin resolves private keys at runtime via aresolvePrivateKeycallback (supporting HSM/KMS without storing keys in the database) or inline viadefaultSSO. Discovery now correctly selectsprivate_key_jwtwhen the IdP requires it.Security properties
exp; in-flight deduplication via process-local Setprivate_key_jwtclients cannot authenticate withclient_secretexprequired, capped byassertionMaxLifetime(default 5 min), advisoryiatcheck when presentSummary 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
@better-auth/oauth-provider): Accept/verifyclient_assertionon token/introspect/revoke; register keys viajwksor HTTPSjwks_uri(mutually exclusive); enforce auth method; cap assertion lifetime viaassertionMaxLifetime(default 5m); preventjtireplay; 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; unifiedextractClientCredentials()for Basic/POST/private_key_jwt.@better-auth/core,@better-auth/sso,generic-oauth):signClientAssertion()utility; auth code, refresh, and client‑credentials requests supportauthentication: "private_key_jwt"withclientAssertion(JWK or PKCS#8 PEM, optional pre‑signed,expiresIn,audfromtokenEndpoint); SSO discovery selectsprivate_key_jwtwhen the IdP requires it and signs viaresolvePrivateKeyor inline key (validated on update); SSOclientSecretis optional forprivate_key_jwt; newprivateKeyId/privateKeyAlgorithmfields;generic-oauthauto‑passestokenEndpointand assertion across flows. Also fixes base64 encoding in client‑credentials token requests.Migration
token_endpoint_auth_method: "private_key_jwt"and providejwksor HTTPSjwks_uri. Switching auth methods auto‑clears incompatible credentials.private_key_jwt, configureresolvePrivateKey(HSM/KMS supported) or provide an inline key;clientSecretbecomes optional; setprivateKeyId/privateKeyAlgorithmif needed.generic-oauth: setauthentication: "private_key_jwt"and provideclientAssertion; the plugin auto‑passestokenEndpointfor signing.Written for commit 1a4c1d6. Summary will update on new commits.