different-ai/openwork · error · EnterpriseMcpOAuthContractError
MCP_OAUTH_CREDENTIAL_EXPIRED
MCP_OAUTH_CREDENTIAL_EXPIRED
Error message
The OAuth access token has expired and no refresh token is available.
What it means
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.
Source
Thrown at packages/enterprise-mcp-client/src/oauth-provider.ts:392
async tokens(context?: OAuthClientInformationContext): Promise<StoredOAuthTokens | undefined> {
const record = await this.persistence.credentials.load(this.context())
if (!record) {
this.loadedCredential = undefined
return undefined
}
const tokens = this.storedTokens(record.tokens, context)
if (!record.revision.trim()) {
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_PERSISTENCE_INVALID",
"The OAuth credential is missing its persistence revision.",
)
}
if (record.expiresAt !== undefined) {
assertFiniteEpoch(record.expiresAt, "token expiration")
if (record.expiresAt <= this.clock.now() + this.expirationSkewMs && !tokens.refresh_token) {
await this.persistence.credentials.invalidate({ context: this.context(), reason: "expired" })
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_CREDENTIAL_EXPIRED",
"The OAuth access token has expired and no refresh token is available.",
)
}
}
this.loadedCredential = { ...record, tokens }
return tokens
}
async saveTokens(tokens: StoredOAuthTokens, context?: OAuthClientInformationContext): Promise<void> {
const validated = this.storedTokens(tokens, context)
const source = this.authorizationHandle ? "authorization-code" : "refresh"
const existing = source === "refresh"
? (this.loadedCredential ?? await this.persistence.credentials.load(this.context()))
: undefined
const merged = source === "refresh" && !validated.refresh_token && existing?.tokens.refresh_token
? { ...validated, refresh_token: existing.tokens.refresh_token }
: validatedView on GitHub (pinned to 2b7df46e8a)
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.
Example fix
// before scope: "read" // after — request a refresh token scope: "read offline_access"
Defensive patterns
Strategy: try-catch
Validate before calling
const record = await persistence.credentials.load(context) const expiredNoRefresh = record?.expiresAt !== undefined && record.expiresAt <= Date.now() && !record.tokens.refresh_token
Try / catch
try { tokens = await provider.tokens() }
catch (e) {
if (e instanceof EnterpriseMcpOAuthContractError && e.code === "MCP_OAUTH_CREDENTIAL_EXPIRED") {
return promptReauthorization() // silent renewal impossible
}
throw e
} Prevention
- 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
When it happens
Trigger: tokens() is called for a stored record where expiresAt <= clock.now() + expirationSkewMs and tokens.refresh_token is undefined.
Common situations: 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.
Related errors
- Google Workspace refresh token is missing. Reconnect Google
- Google OAuth refresh did not return an access token.
- invalid_github_install_state
- `${t("providers.no_oauth_prefix")} ${resolved}. ${t("provide
- `${t("providers.not_oauth_flow_prefix")} ${resolved}.`
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/f6915b4e6add9606.
Report an issue: GitHub.