{"record":{"id":"f6915b4e6add9606","repo":"different-ai/openwork","slug":"mcp-oauth-credential-expired","errorCode":"MCP_OAUTH_CREDENTIAL_EXPIRED","errorMessage":"The OAuth access token has expired and no refresh token is available.","messagePattern":"The OAuth access token has expired and no refresh token is available\\.","errorType":"exception","errorClass":"EnterpriseMcpOAuthContractError","httpStatus":null,"severity":"error","filePath":"packages/enterprise-mcp-client/src/oauth-provider.ts","lineNumber":392,"sourceCode":"\n  async tokens(context?: OAuthClientInformationContext): Promise<StoredOAuthTokens | undefined> {\n    const record = await this.persistence.credentials.load(this.context())\n    if (!record) {\n      this.loadedCredential = undefined\n      return undefined\n    }\n    const tokens = this.storedTokens(record.tokens, context)\n    if (!record.revision.trim()) {\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_PERSISTENCE_INVALID\",\n        \"The OAuth credential is missing its persistence revision.\",\n      )\n    }\n    if (record.expiresAt !== undefined) {\n      assertFiniteEpoch(record.expiresAt, \"token expiration\")\n      if (record.expiresAt <= this.clock.now() + this.expirationSkewMs && !tokens.refresh_token) {\n        await this.persistence.credentials.invalidate({ context: this.context(), reason: \"expired\" })\n        throw new EnterpriseMcpOAuthContractError(\n          \"MCP_OAUTH_CREDENTIAL_EXPIRED\",\n          \"The OAuth access token has expired and no refresh token is available.\",\n        )\n      }\n    }\n    this.loadedCredential = { ...record, tokens }\n    return tokens\n  }\n\n  async saveTokens(tokens: StoredOAuthTokens, context?: OAuthClientInformationContext): Promise<void> {\n    const validated = this.storedTokens(tokens, context)\n    const source = this.authorizationHandle ? \"authorization-code\" : \"refresh\"\n    const existing = source === \"refresh\"\n      ? (this.loadedCredential ?? await this.persistence.credentials.load(this.context()))\n      : undefined\n    const merged = source === \"refresh\" && !validated.refresh_token && existing?.tokens.refresh_token\n      ? { ...validated, refresh_token: existing.tokens.refresh_token }\n      : validated","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/enterprise-mcp-client/src/oauth-provider.ts#L374-L410","documentation":"Thrown from tokens when the persisted access token's expiresAt is at or past now (minus the expiration skew window) and the record has no refresh_token. Without a refresh token there is no way to renew silently, so the library invalidates the stored credential and forces a full re-authorization. Note: an expired token WITH a refresh token does not throw — the flow refreshes instead.","triggerScenarios":"tokens() is called for a stored record where expiresAt <= clock.now() + expirationSkewMs and tokens.refresh_token is undefined.","commonSituations":"Authorization server that issues short-lived access tokens without refresh tokens (or refresh revoked/one-time-use consumed); long-lived app session outliving a short token; clock skew between client and server trimming the usable window.","solutions":["Start a new connection/authorization flow to obtain fresh tokens.","Ensure the authorization flow requests the offline_access/refresh_token scope so a refresh token is stored.","Check server config for refresh token rotation or revocation that leaves the stored token unrefreshable; verify system clock is not skewed."],"exampleFix":"// before\nscope: \"read\"\n// after — request a refresh token\nscope: \"read offline_access\"","handlingStrategy":"try-catch","validationCode":"const record = await persistence.credentials.load(context)\nconst expiredNoRefresh = record?.expiresAt !== undefined\n  && record.expiresAt <= Date.now()\n  && !record.tokens.refresh_token","typeGuard":null,"tryCatchPattern":"try { tokens = await provider.tokens() }\ncatch (e) {\n  if (e instanceof EnterpriseMcpOAuthContractError && e.code === \"MCP_OAUTH_CREDENTIAL_EXPIRED\") {\n    return promptReauthorization() // silent renewal impossible\n  }\n  throw e\n}","preventionTips":["Request offline_access (or equivalent) scope so a refresh token is stored","Surface re-auth prompts proactively when tokens near expiry","Keep server clocks NTP-synced to avoid premature expiry from skew"],"tags":["oauth","token-expired","refresh-token","reauthentication"],"backgroundTag":"oauth-access-token-expired","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}