#11318·better-auth

oauth-provider: consent deletion leaves pending codes redeemable

Author: andreiguzgaCreated Sep 17, 2026Updated Sep 17, 2026

Is this suited for GitHub?

  • Yes, this is suited for GitHub

Reproduction

Authorization codes are not bound to the consent row that authorized them. Deleting that consent therefore leaves an already-issued code redeemable; if the user later grants replacement consent, an access-token claims extension that resolves consent at mint time can attach the replacement consent to the old authorization.

  1. Authorize user U for client C and create consent C1.
  2. Complete authorization and retain code A without exchanging it.
  3. Delete C1 through the user consent-removal flow.
  4. Authorize U for C again, creating replacement consent C2 and code B.
  5. Exchange B and verify it succeeds.
  6. Exchange the older code A.

Observed result for step 6: HTTP 200 with tokens.

Expected result for step 6: HTTP 400 with invalid_grant, because the grant/consent under which A was authorized no longer exists.

The issue is also visible in the 1.7.5 source:

An extension that adds a consent identifier to resource-token claims cannot close the race: its accessToken hook runs at token mint time, after A is redeemed, so it can only see C2 and cannot distinguish A from code B.

Current vs. expected behavior

Current: removing a consent does not invalidate pending authorization codes. A code issued under deleted consent C1 can be exchanged after reconnect and can inherit state or claims derived from replacement consent C2.

Expected: a pending code must remain bound to its authorization-time grant. If that grant is deleted or superseded before token exchange, redemption must fail with invalid_grant. A fresh code issued under C2 must continue to work.

Deleting the Auth session happens to reject its pending codes, but per-client consent removal intentionally leaves the login session active, so session existence is not a substitute for grant binding.

What version of Better Auth are you using?

Reproduced on [email protected] and @better-auth/[email protected]. The latest 1.7.5 source was reviewed and still stores no authorization-time consent identifier. The complete reproduction has not been rerun on 1.7.5.

System info

bash
OS: Darwin 25.6.0 arm64
Node: 22.21.0
pnpm: 11.21.0
Database: PostgreSQL
Adapter: Prisma 6.19.3
Framework: Next.js 16.3.4
Test runner: Vitest 4.1.11

Which areas are affected?

  • Backend
  • Package

Auth config

typescript
oauthProvider({
  grantTypes: ["authorization_code"],
  scopes: ["openid", "profile", "email", "api.read"],
  clientRegistrationRequirePKCE: true,
  accessTokenExpiresIn: 300,
  codeExpiresIn: 300,
});

The deterministic reproduction uses a confidential client plus S256 PKCE and exact redirect URI matching. Code one-time use and PKCE remain enforced; the missing property is authorization-time consent binding.

Additional context

  • #10386 and open PR #11056 address revoking already-issued access and refresh tokens when consent is deleted. They do not bind or delete pending authorization codes.
  • Automated review on PR #11056 also notes that concurrent authorization can issue credentials around consent deletion, but the pending-code/replacement-consent schedule above is independently deterministic.
  • Binding only user/client/reference/scopes/resources is insufficient because replacement consent can have the same values. The code needs an authorization-time consent identity or equivalent grant-generation marker that deletion/revocation can invalidate.