Skip to content

feat(oauth-provider): add refresh token reuse interval - #10145

Merged
gustavovalverde merged 1 commit into
nextfrom
codex/refresh-token-reuse-interval
Jun 26, 2026
Merged

gustavovalverde merged 1 commit into
nextfrom
codex/refresh-token-reuse-interval

Conversation

@gustavovalverde

@gustavovalverde gustavovalverde commented Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

Duplicate refresh-token requests can invalidate the refresh-token family after one request wins rotation, even when the second request is only a retry or local race.

This adds refreshTokenReuseInterval as an OAuth Provider opt-in: provider default stays 0, so strict replay detection is unchanged unless configured. mcp() deliberately defaults it to 30 seconds because native/public MCP clients can retry with the old refresh token after another local session already consumed it.

Inside that interval, Better Auth replays the original rotated response only for an equivalent request: same client, effective scopes, requested resources, and sender constraint. Non-equivalent reuse returns invalid_grant without family invalidation during the interval; expired reuse keeps the existing family-invalidation behavior.

Closes #8512

@vercel

vercel Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
better-auth Ready Ready Preview, Comment Jun 26, 2026 3:20am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
better-auth-demo Ignored Ignored Jun 26, 2026 3:20am

@better-release better-release Bot added identity OAuth/OIDC provider, device flow docs Documentation, demos labels Jun 19, 2026
@better-release
better-release Bot changed the base branch from main to next June 19, 2026 04:55
@better-release

Copy link
Copy Markdown
Contributor

This PR was automatically retargeted from main to next because it contains a minor changeset. The main branch only accepts patch (bug fix) changes. Features and breaking changes go through next for beta testing before promotion to stable.

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@better-auth/api-key

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

better-auth

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

@better-auth/cimd

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

auth

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

@better-auth/core

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

@better-auth/drizzle-adapter

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

@better-auth/electron

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

@better-auth/expo

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

@better-auth/i18n

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

@better-auth/kysely-adapter

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

@better-auth/mcp

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

@better-auth/memory-adapter

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

@better-auth/mongo-adapter

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

@better-auth/oauth-provider

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

@better-auth/passkey

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

@better-auth/prisma-adapter

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

@better-auth/redis-storage

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

@better-auth/scim

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

@better-auth/sso

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

@better-auth/stripe

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

@better-auth/telemetry

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

@better-auth/test-utils

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

commit: ccfe0fd

@gustavovalverde
gustavovalverde requested a review from Copilot June 25, 2026 19:06
@gustavovalverde
gustavovalverde marked this pull request as ready for review June 25, 2026 19:06
@gustavovalverde
gustavovalverde requested review from a team as code owners June 25, 2026 19:06
@gustavovalverde
gustavovalverde requested review from ping-maxwell and removed request for a team June 25, 2026 19:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an opt-in refresh-token “reuse interval” for the OAuth Provider plugin so duplicate refresh-token requests within a short window can replay the original token response (instead of being treated as a replay attack that invalidates the token family). It also includes several related fixes and maintenance updates across i18n, session refresh cookies, logging, tooling overrides, and docs.

Changes:

  • Add refreshTokenReuseInterval to @better-auth/oauth-provider, persisting rotation metadata and an encrypted cached token response for safe replay during the configured window.
  • Update i18n default-locale fallback behavior to default to "en" and adjust tests/docs accordingly.
  • Misc fixes/updates: session refresh cookie Max-Age behavior, OAuth account-linking logging via configured logger, TypeScript typing improvements for APIError, and esbuild override bumps for demos/e2e.

Reviewed changes

Copilot reviewed 27 out of 30 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Updates esbuild override range.
packages/oauth-provider/src/types/index.ts Adds refreshTokenReuseInterval option and new refresh-token rotation/replay fields.
packages/oauth-provider/src/token.ts Implements rotation replay storage/encryption and reuse-interval replay behavior in the refresh grant flow.
packages/oauth-provider/src/token.test.ts Adds test coverage for reuse-interval replay behavior and related edge cases.
packages/oauth-provider/src/schema.ts Extends oauthRefreshToken schema with rotation/replay metadata fields.
packages/oauth-provider/src/schema.test.ts Ensures new indexed FK field is covered by schema index assertions.
packages/oauth-provider/src/oauth.ts Sets default refreshTokenReuseInterval: 0 in provider defaults.
packages/i18n/src/index.ts Changes default-locale selection to default to "en" when translations exist.
packages/i18n/src/i18n.test.ts Updates tests to reflect new i18n fallback behavior.
packages/core/src/error/index.ts Improves TS inference by declaring inherited APIError properties.
packages/better-auth/src/oauth2/link-account.ts Routes account-linking logs through the configured request context logger.
packages/better-auth/src/oauth2/link-account.test.ts Adds regression test ensuring custom logger receives link-account errors.
packages/better-auth/src/api/routes/session.ts Adjusts session refresh cookie Max-Age handling to avoid exceeding browser ceiling.
packages/better-auth/src/api/routes/session-api.test.ts Adds regression test for 400-day Max-Age ceiling on session refresh.
e2e/smoke/test/fixtures/esbuild/package.json Bumps esbuild devDependency used in smoke fixture.
e2e/integration/solid-vinxi/app.config.ts Forces modern Vite/esbuild target to avoid down-level destructuring build issues.
docs/lib/copy-schema/adapter/drizzle.ts Adjusts Drizzle bigint mapping behavior for non-sqlite providers.
docs/content/docs/plugins/oauth-provider.mdx Documents refreshTokenReuseInterval option and new refresh-token fields.
docs/content/docs/plugins/i18n.mdx Updates i18n docs to reflect "en" default behavior and fallback rules.
demo/oidc-client/pnpm-workspace.yaml Adds esbuild override for the OIDC demo workspace.
demo/oidc-client/pnpm-lock.yaml Lockfile update reflecting esbuild override/bump.
demo/electron/pnpm-workspace.yaml Adds esbuild override for the Electron demo workspace.
demo/electron/pnpm-lock.yaml Lockfile update reflecting esbuild override/bump.
.greptile/config.json Updates Greptile automation settings (triggering/status check/authors).
.changeset/vast-houses-joke.md Changeset entry for i18n fallback behavior update.
.changeset/sparkly-items-shave.md Changeset entry for APIError typing improvement.
.changeset/red-islands-prove.md Changeset entry for session refresh cookie Max-Age behavior.
.changeset/quiet-tokens-retry.md Changeset entry for OAuth Provider reuse-interval feature.
.changeset/oauth-account-linking-logger.md Changeset entry for logger usage in OAuth account-linking/create-user paths.
Files not reviewed (2)
  • demo/electron/pnpm-lock.yaml: Generated file
  • demo/oidc-client/pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (1)

packages/oauth-provider/src/token.ts:554

  • When the CAS update in createRefreshToken loses (!won), the request immediately returns invalid_grant even if refreshTokenReuseInterval is enabled. In the common “two concurrent refresh calls” case, the loser is an equivalent duplicate request and should ideally receive the same cached token response once the winner finishes rotation, rather than failing. Consider adding a post-CAS-loss path that re-loads the parent refresh row and replays rotationReplayResponse (when present and request matches) instead of always throwing invalid_grant.
	if (!won) {
		throw new APIError("BAD_REQUEST", {
			error_description: "invalid refresh token",
			error: "invalid_grant",
		});

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

Comment thread packages/oauth-provider/src/token.ts Outdated
Comment thread packages/i18n/src/index.ts
@greptile-apps

greptile-apps Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces refreshTokenReuseInterval to the OAuth Provider, allowing a rotated (revoked) refresh token to replay its rotation response for a configurable window when the new request carries an equivalent fingerprint (same effective scopes, requested resources, and sender constraint). The mcp() helper defaults the interval to 30 seconds to handle native/public MCP clients that can race the same refresh token across local sessions.

  • Schema: three new columns (rotatedAt, rotationReplayResponse, rotationReplayExpiresAt) are added to oauthRefreshToken; rotation now always records rotatedAt regardless of whether the interval is enabled.
  • Token logic: the revoked-token check was repositioned to after validateClientCredentials; within the reuse interval, a cryptographically encrypted fingerprint-matched replay is returned rather than triggering family invalidation; outside the interval (or when reuseInterval = 0), the existing family-invalidation path is unchanged.
  • Previous thread concerns addressed: scope/resource order-insensitive comparison via normalizeReplayValues, expires_in recomputed from expires_at on replay, and store failures handled with try/catch + logger so the freshly-issued token is always returned.

Confidence Score: 5/5

Safe to merge; the feature is opt-in with a provider default of 0 so existing deployments are unaffected, and family invalidation continues to work correctly via hard deletes.

The three previously-flagged issues (scope ordering, stale expires_in, store-failure user lockout) are all addressed. Family invalidation still hard-deletes token rows, so the replay window is automatically terminated for any token the invalidation covers. The remaining note — that rotationReplayExpiresAt is evaluated from the stored row rather than the live config, so lowering the interval does not shorten already-set windows — is a narrow operational limitation bounded by the configured TTL and does not represent a current defect.

packages/oauth-provider/src/token.ts — specifically the isWithinRefreshTokenReuseInterval helper and the replay-store path in createUserTokens

Reviews (4): Last reviewed commit: "feat(oauth-provider): add refresh token ..." | Re-trigger Greptile

Comment thread packages/oauth-provider/src/token.ts Outdated
Comment thread packages/oauth-provider/src/token.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3 issues found across 30 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="packages/i18n/src/index.ts">

<violation number="1" location="packages/i18n/src/index.ts:78">
P2: `defaultLocale` is typed as `Locales[number]`, but this branch unconditionally casts `"en" as Locales[number]` even when `"en"` is not present in the translations dictionary. This makes the generic type contract unsound—consumers relying on the `Locales` type parameter could receive a locale value that doesn't belong to their defined set. Consider widening the type (e.g., `Locales[number] | "en"`) or adding a runtime guard.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/oauth-provider/src/token.test.ts
Comment thread packages/oauth-provider/src/token.ts Outdated
Comment thread packages/i18n/src/index.ts
@gustavovalverde
gustavovalverde force-pushed the codex/refresh-token-reuse-interval branch from 6528bf2 to fac6d6b Compare June 25, 2026 19:34
Comment thread packages/oauth-provider/src/token.ts
@gustavovalverde
gustavovalverde force-pushed the codex/refresh-token-reuse-interval branch from fac6d6b to fa2747c Compare June 25, 2026 20:04
@gustavovalverde
gustavovalverde force-pushed the codex/refresh-token-reuse-interval branch from fa2747c to ccfe0fd Compare June 26, 2026 03:19
@gustavovalverde
gustavovalverde added this pull request to the merge queue Jun 26, 2026
Merged via the queue into next with commit 5838df2 Jun 26, 2026
31 checks passed
@gustavovalverde
gustavovalverde deleted the codex/refresh-token-reuse-interval branch June 26, 2026 03:57
@better-release better-release Bot added the locked Locked conversations after being closed for 7 days label Jul 4, 2026
@better-release better-release Bot locked as resolved and limited conversation to collaborators Jul 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

docs Documentation, demos identity OAuth/OIDC provider, device flow locked Locked conversations after being closed for 7 days

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refresh Token Rotation Grace Period (Overlap Window)

2 participants