[PR #8636] [CLOSED] feat(oauth-provider): plugin-extensible token endpoint #25009

Closed
opened 2026-04-15 22:41:20 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/8636
Author: @gustavovalverde
Created: 3/16/2026
Status: Closed

Base: canaryHead: feat/token-exchange


📝 Commits (2)

  • d12c2bc feat(oauth-provider): extensible grant types and passthrough token body
  • 4c20858 fix: harden token endpoint extensibility for plugin grant types

📊 Changes

6 files changed (+217 additions, -81 deletions)

View changed files

📝 docs/content/docs/plugins/oauth-provider.mdx (+8 -2)
📝 packages/oauth-provider/src/index.ts (+1 -0)
📝 packages/oauth-provider/src/oauth.ts (+17 -34)
📝 packages/oauth-provider/src/token.test.ts (+125 -0)
📝 packages/oauth-provider/src/token.ts (+59 -38)
📝 packages/oauth-provider/src/types/oauth.ts (+7 -7)

📄 Description

Problem

The token endpoint is the convergence point for every feature that touches the OAuth token flow. Grant dispatch, token creation, id_token creation, and grant-specific handlers all live in token.ts. When the endpoint lacks extension points, every plugin that adds a grant type must modify the same shared code. Features that are functionally independent (CIBA, token exchange, device authorization) become structurally coupled through edits to the same file and the same type definitions.

Two specific constraints enforce this coupling:

  1. Zod strips unknown fields. The body schema uses z.object({...}) which discards any field not declared in the schema. A plugin that needs auth_req_id (CIBA), subject_token (token exchange), or client_assertion (JWT auth) must add its field to the core schema, creating a dependency between the plugin and oauth-provider's source.

  2. grant_type is a closed enum. z.enum(["authorization_code", "client_credentials", "refresh_token"]) rejects any value outside the three built-in types before the handler runs. The switch statement's default case throws unconditionally. A plugin cannot register a custom grant type without modifying both the Zod schema and the switch statement in token.ts.

The GrantType TypeScript type reinforces this: it's a closed union, so code that checks opts.grantTypes.includes(grantType) requires a cast when the grant type is a plugin-defined string. OAuth 2.x grant types are extensible URIs by spec (RFC 6749 §8.5); the type system should reflect this rather than fight it.

Changes

Type layer

GrantType is now type GrantType = string. The alias preserves semantic meaning in signatures (grantTypes?: GrantType[] reads better than grantTypes?: string[]) without constraining the runtime set. No casts needed anywhere.

Zod schema

  • grant_type widened to z.string().trim().min(1)
  • .passthrough() on the token endpoint body, so plugin-specific fields survive Zod parsing without being pre-declared in the core schema
  • DCR grant_types widened to z.array(z.string()) per RFC 7591 §2 (the spec defines this field as an array of grant type strings, not a closed set)

Dispatch

The switch default case looks up customGrantTypeHandlers from the endpoint context before throwing unsupported_grant_type. Plugins register handlers via init():

init(ctx) {
  return {
    context: {
      customGrantTypeHandlers: {
        [MY_GRANT_TYPE]: myHandler,
      },
    },
  };
}

With these three changes, a plugin that adds a grant type (CIBA, token exchange, device authorization) touches zero lines in oauth-provider. It registers its handler, declares its grant type string, and reads its own fields from the passthrough body.

Docs

  • Updated token endpoint section to mention plugin extensibility
  • Updated grantTypes config description
  • Fixed DCR OpenAPI description (was "Requested authentication method")

References


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/better-auth/better-auth/pull/8636 **Author:** [@gustavovalverde](https://github.com/gustavovalverde) **Created:** 3/16/2026 **Status:** ❌ Closed **Base:** `canary` ← **Head:** `feat/token-exchange` --- ### 📝 Commits (2) - [`d12c2bc`](https://github.com/better-auth/better-auth/commit/d12c2bc73af019b250407cdd752a74946a912447) feat(oauth-provider): extensible grant types and passthrough token body - [`4c20858`](https://github.com/better-auth/better-auth/commit/4c208586ad5f034f438473f96c6f42809846a08a) fix: harden token endpoint extensibility for plugin grant types ### 📊 Changes **6 files changed** (+217 additions, -81 deletions) <details> <summary>View changed files</summary> 📝 `docs/content/docs/plugins/oauth-provider.mdx` (+8 -2) 📝 `packages/oauth-provider/src/index.ts` (+1 -0) 📝 `packages/oauth-provider/src/oauth.ts` (+17 -34) 📝 `packages/oauth-provider/src/token.test.ts` (+125 -0) 📝 `packages/oauth-provider/src/token.ts` (+59 -38) 📝 `packages/oauth-provider/src/types/oauth.ts` (+7 -7) </details> ### 📄 Description ## Problem The token endpoint is the convergence point for every feature that touches the OAuth token flow. Grant dispatch, token creation, id_token creation, and grant-specific handlers all live in `token.ts`. When the endpoint lacks extension points, every plugin that adds a grant type must modify the same shared code. Features that are functionally independent (CIBA, token exchange, device authorization) become structurally coupled through edits to the same file and the same type definitions. Two specific constraints enforce this coupling: 1. **Zod strips unknown fields.** The body schema uses `z.object({...})` which discards any field not declared in the schema. A plugin that needs `auth_req_id` (CIBA), `subject_token` (token exchange), or `client_assertion` (JWT auth) must add its field to the core schema, creating a dependency between the plugin and oauth-provider's source. 2. **`grant_type` is a closed enum.** `z.enum(["authorization_code", "client_credentials", "refresh_token"])` rejects any value outside the three built-in types before the handler runs. The switch statement's default case throws unconditionally. A plugin cannot register a custom grant type without modifying both the Zod schema and the switch statement in token.ts. The `GrantType` TypeScript type reinforces this: it's a closed union, so code that checks `opts.grantTypes.includes(grantType)` requires a cast when the grant type is a plugin-defined string. OAuth 2.x grant types are extensible URIs by spec (RFC 6749 §8.5); the type system should reflect this rather than fight it. ## Changes ### Type layer `GrantType` is now `type GrantType = string`. The alias preserves semantic meaning in signatures (`grantTypes?: GrantType[]` reads better than `grantTypes?: string[]`) without constraining the runtime set. No casts needed anywhere. ### Zod schema - `grant_type` widened to `z.string().trim().min(1)` - `.passthrough()` on the token endpoint body, so plugin-specific fields survive Zod parsing without being pre-declared in the core schema - DCR `grant_types` widened to `z.array(z.string())` per RFC 7591 §2 (the spec defines this field as an array of grant type strings, not a closed set) ### Dispatch The switch default case looks up `customGrantTypeHandlers` from the endpoint context before throwing `unsupported_grant_type`. Plugins register handlers via `init()`: ```ts init(ctx) { return { context: { customGrantTypeHandlers: { [MY_GRANT_TYPE]: myHandler, }, }, }; } ``` With these three changes, a plugin that adds a grant type (CIBA, token exchange, device authorization) touches zero lines in oauth-provider. It registers its handler, declares its grant type string, and reads its own fields from the passthrough body. ### Docs - Updated token endpoint section to mention plugin extensibility - Updated `grantTypes` config description - Fixed DCR OpenAPI description (was "Requested authentication method") ## References - [RFC 6749 §8.5 — Defining New Grant Types](https://datatracker.ietf.org/doc/html/rfc6749#section-8.5) - [RFC 7591 §2 — Client Metadata (`grant_types`)](https://datatracker.ietf.org/doc/html/rfc7591#section-2) - [RFC 8693 — OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693) - [CIBA Core §10 — Token Request](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html#rfc.section.10) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-15 22:41:20 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#25009