Skip to content

fix(oauth): add opt-in bounded grace period for refresh-token rotation - #387

Open
kotovasia5120 wants to merge 1 commit into
Waishnav:mainfrom
kotovasia5120:fix/oauth-refresh-grace-period
Open

kotovasia5120 wants to merge 1 commit into
Waishnav:mainfrom
kotovasia5120:fix/oauth-refresh-grace-period

Conversation

@kotovasia5120

@kotovasia5120 kotovasia5120 commented Oct 5, 2026 •

Copy link
Copy Markdown

Concurrent refreshes or a retry after a lost rotation response can reuse the previous refresh token. DevSpace currently deletes that token immediately, so the retry receives invalid_grant and a host can fall back to the owner-password authorization flow even though the original token TTL has not elapsed.

This adds opt-in oauth.refreshTokenGraceSeconds (0–600, default 0). When enabled, rotation atomically shortens the consumed token's expiry to the earlier of its existing expiry and the grace deadline. Subsequent retries cannot move that deadline, and each newly issued refresh token keeps the configured TTL. The default retains immediate consumption; default access/refresh TTLs, client/resource/scope checks, and explicit token revocation are unchanged. No database migration is needed. Enabling grace deliberately permits replay of a rotated token within the selected window, so the option is documented as a compatibility tradeoff for trusted hosts.

Behavioral regression tests use real SQLite and a controlled clock for duplicate refreshes through two providers, the fixed 600-second boundary, shorter original lifetimes, persistence after all connections close, revocation, and client/resource/scope restrictions. Configuration validation, generated JSON schema, and configuration loading are covered as well.

The focused OAuth/config tests, pnpm typecheck, pnpm build, and the full pnpm test suite passed locally on Windows (142 passed, 7 skipped). The initial full run hit two existing workspace assertions comparing C:\Temp with Windows' canonical C:\temp; aligning this process's TEMP/TMP with that canonical path made both the focused workspace tests and the full suite pass without workspace code changes. Platform-specific tests were skipped, and Pi sandbox integration was unavailable in this environment.

A private deployment using an equivalent fixed 600-second patch also completed a subsequent ChatGPT refresh without another owner-password prompt; that observation is separate from verification of this upstream implementation.

Summary by CodeRabbit

  • New Features
    • OAuth refresh tokens can now be retried for a configurable grace period of 0–600 seconds after rotation. The default is 0 seconds, which consumes tokens immediately.
    • Retries use the original grace deadline and do not extend it. Client, scope, resource, and revocation checks still apply.
  • Documentation
    • Added configuration guidance, including the security implications of allowing rotated tokens to be reused during the grace period.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4ebeff79-d9ca-477d-8aba-5f9ebb01c71f
📥 Commits

Reviewing files that changed from the base of the PR and between 531d3f9 and 868a8ca.

📒 Files selected for processing (9)
  • docs/configuration.md
  • schema/v1/devspace.schema.json
  • src/config-schema.test.ts
  • src/config-schema.ts
  • src/config.test.ts
  • src/config.ts
  • src/oauth-provider.ts
  • src/oauth-refresh-grace.test.ts
  • src/oauth-store.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Adds an OAuth refresh-token grace setting from 0 to 600 seconds. The provider passes it to the SQLite store, which applies the grace deadline to consumed refresh tokens. Configuration and token behavior are covered by tests and documentation.

Changes

OAuth Refresh-Token Grace Period

Layer / File(s) Summary
Configure the grace period
src/config-schema.ts, schema/v1/devspace.schema.json, src/config-schema.test.ts, src/config.ts, src/config.test.ts, src/oauth-provider.ts, docs/configuration.md
Adds refreshTokenGraceSeconds, defaulting to 0 and limited to integer values from 0 through 600. Loads the setting and passes it to the store. Adds configuration validation and default-value tests.
Apply and verify token grace
src/oauth-store.ts, src/oauth-refresh-grace.test.ts, docs/configuration.md
When grace is positive, the store shortens a consumed token’s expiry to the earlier of its existing expiry and the grace deadline. Tests cover retries, expiry limits, persistence, revocation, and refresh-policy checks. Documentation describes the behavior and its constraints.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SingleUserOAuthProvider
  participant SqliteOAuthStore
  Client->>SingleUserOAuthProvider: Submit refresh token
  SingleUserOAuthProvider->>SqliteOAuthStore: Save token pair with consumed token hash
  SqliteOAuthStore->>SqliteOAuthStore: Apply grace deadline to consumed token
  SingleUserOAuthProvider->>Client: Return token pair
Loading

Suggested reviewers: waishnav

Merge Risk: ⚪ Minimal · up to 868a8

This adds an opt-in, bounded refresh-token grace period that is off by default, so existing deployments behave as before. No blocking issues were found, and the replay tradeoff is documented.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 868a8

The feature deliberately lets a previously rotated credential mint additional tokens. Those replacement credentials can remain usable beyond the retry window. The setting is disabled by default, and client, resource, scope, and atomic rotation controls remain intact. The main risk is the accepted replay tradeoff when an operator enables it, rather than an established authorization bypass.

Retained concerns

  • Medium · security · observed: With grace enabled, possession of a previously consumed refresh token remains sufficient at the provider boundary to mint independent replacement credentials. The old token's bounded deadline does not bound the lifetime of those replacements, and revoking one replacement does not invalidate sibling credentials. This is an intentional, opt-in expansion of replay exposure, not a verified bypass of client, scope, or resource checks.
Security review details

Security Blast Radius

  • inferred — The affected authority is the credential's registered client, persisted scopes, and matching allowed resource on the OAuth-backed DevSpace instance. Replacement access tokens can reach the protected MCP surface when its required scope and resource checks pass. The documented surface includes file modification and shell tools; deployment-specific sandbox and credential reach are unresolved.

Security Findings and Attack Paths

  • inferred — An attacker who obtains an already-rotated refresh token can, with grace enabled and an appropriate client identity supplied, exchange it before its retained deadline for credentials that outlive that deadline. This extends exposure beyond base behavior, where that consumed token was deleted. It requires possession of the credential and remains subject to provider-side authorization checks; external HTTP dispatch and client-authentication semantics were not fully verified.

Trust Boundaries and Controls

  • observed — Grace does not remove the provider's client, resource, or scope checks. Regression tests reject another registered client, an unauthorized scope, and a different resource after rotation. The protected MCP route separately requires bearer verification and an allowed resource.

Resilience and Maintainability Implications

  • observed — Revocation deletes the specifically supplied token hash. This behavior predates the grace change and still prevents reuse of that revoked credential, but it does not cascade to independent pairs created by other accepted exchanges. Expiry and individual revocation therefore should not be interpreted as lineage-wide compromise recovery.

Hardening Proposals

  • proposed — Document that disabling grace or revoking one token does not invalidate all credentials previously minted through replay. If deployments require compromise-wide recovery, evaluate explicit session or token-family invalidation rather than relying on the grace deadline alone.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 7 files. (2 skipped: 2 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the opt-in, bounded grace period for refresh-token rotation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 7 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the tokens’ time,
A grace window makes retries align.
The old token keeps its bound,
New tokens pass the tests around.
Then hops away, content and fine.

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Critical risk] Adds grace period for reusing rotated refresh tokens.

The identified test-coverage gap does not block merging.

Findings

  1. P2 Refresh calls do not overlap ▶

Summary

Adds an opt-in, bounded refresh-token rotation grace period while retaining immediate consumption by default. It also updates configuration, schema, documentation, and SQLite-backed tests. The new two-connection test does not exercise overlapping database rotations; this test-coverage gap is non-blocking.

Reviews (1) · Last reviewed commit: "fix(oauth): add opt-in bounded grace per..."

Comment on lines +66 to +69
const [leftTokens, rightTokens] = await Promise.all([
left.exchangeRefreshToken(client, tokens.refresh_token),
right.exchangeRefreshToken(client, tokens.refresh_token),
]);

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.

P2 Refresh calls do not overlap

Both refresh calls complete their synchronous SQLite work one after the other, despite being passed to Promise.all. The test checks reuse through two connections, but cannot catch regressions that appear only when rotations contend for a database write lock. This test-coverage gap is non-blocking.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Artifacts

Executable refresh-rotation comparison script

  • The authored script runs the provider calls with same-event-loop tracing and with an independent process holding a SQLite write lock, enabling a direct comparison.

Original refresh-grace test passing

  • The repository's refresh-grace test was run unchanged and passed, showing that its assertions do not require overlapping rotations.

Same-event-loop refresh calls running sequentially

  • Two provider calls in the test's `Promise.all` structure produced distinct tokens, while the trace showed the first transaction exited before the second read began.

Independent-process refresh calls encountering a SQLite lock

  • A separate process held an uncommitted rotation while the second call waited 914 ms for its write lock, showing contention absent from the original test.

View artifacts

T-Rex Ran code and verified through T-Rex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant