Skip to content

vault set treats relative expires_in as immediately due and can invalidate fresh grants #334

Description

@feniix

Summary

mcporter vault set persists an OAuth token carrying expires_in without converting that relative lifetime to an absolute expiry. On the next list or call, mcporter treats the imported token as immediately due and redeems its refresh token even when it was issued seconds ago with a one-hour lifetime.

This can destroy a fresh grant. A headless OAuth owner commonly has to run two separate operations after a callback:

  1. mcporter vault set with the new token and DCR client information.
  2. Update the refreshable_bearer config with that DCR client's ID and token endpoint.

A concurrent probe between those operations can redeem the new refresh token with missing or previous client configuration. Providers that bind a refresh token to its DCR client return invalid_grant; mcporter then correctly clears the rejected tokens but retains clientInfo. The resulting vault entry looks like a successful registration with no token.

Root cause

The two persistence paths handle expires_in differently.

  • VaultPersistence.saveTokens() calls prepareStoredTokens(), whose withStoredExpiry() adds expires_at = now + expires_in.
  • handleVaultSet() calls saveVaultEntry() directly with payload.tokens, so no absolute expiry is recorded.
  • shouldRefreshCachedToken() has no issue time for a relative-only token and returns true whenever both expires_in and refresh_token are present.

The behavior remains present in the published 0.13.7 package.

Reduced behavior proof

I configured one local refreshable_bearer HTTP definition whose token endpoint only counted requests, then seeded this non-secret fixture through the real CLI:

{
  "tokens": {
    "access_token": "fixture-access-token",
    "token_type": "Bearer",
    "refresh_token": "fixture-refresh-token",
    "expires_in": 3600
  },
  "clientInfo": {
    "client_id": "fixture-client"
  }
}
mcporter 0.13.4
vault set exit:       0
first list immediately after set:
  token redemptions:  1

The same proof on 0.13.7 also enters the relative-expiry refresh path. Adding the absolute field that mcporter's normal save path would have derived changes only this result:

"expires_at": <current epoch seconds + 3600>
first list immediately after set:
  token redemptions:  0

No live provider or real credential was used.

Expected behavior

vault set should persist imported tokens with the same expiry normalization as VaultPersistence.saveTokens(). A token seeded with expires_in: 3600 should not be refreshed until its actual expiry window.

A focused regression could freeze time, call the vault-set path with relative expiry, read the stored entry, and assert the derived expires_at. A runtime-level test can additionally assert that the first cached-token read performs zero token requests.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Urgent regression or broken agent/channel workflow affecting real users now.clawsweeper:current-main-reproClawSweeper found a high-confidence current-main issue reproduction.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.impact:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.issue-rating: 🦀 challenger crabExceptional issue quality: high-confidence current-main reproduction and actionable evidence.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions