feat(oauth-provider): add refresh token reuse interval - #10145
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
This PR was automatically retargeted from |
@better-auth/api-key
better-auth
@better-auth/cimd
auth
@better-auth/core
@better-auth/drizzle-adapter
@better-auth/electron
@better-auth/expo
@better-auth/i18n
@better-auth/kysely-adapter
@better-auth/mcp
@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: |
There was a problem hiding this comment.
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
refreshTokenReuseIntervalto@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
createRefreshTokenloses (!won), the request immediately returnsinvalid_granteven ifrefreshTokenReuseIntervalis 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 replaysrotationReplayResponse(when present and request matches) instead of always throwinginvalid_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.
|
There was a problem hiding this comment.
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
6528bf2 to
fac6d6b
Compare
fac6d6b to
fa2747c
Compare
fa2747c to
ccfe0fd
Compare
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
refreshTokenReuseIntervalas an OAuth Provider opt-in: provider default stays0, 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_grantwithout family invalidation during the interval; expired reuse keeps the existing family-invalidation behavior.Closes #8512