Repository navigation
fix(oauth): record revoked refresh tokens durably and surface them as reconnect - #8868
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 28 files
Reply with feedback, questions, or to request a fix.
Turn on auto-fix | Re-trigger cubic
3dc14a4 to
3532741
Compare
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 28 files
Reply with feedback, questions, or to request a fix.
Turn on auto-fix | Re-trigger cubic
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
… reconnect A refresh token the provider revoked (invalid_grant and kin) was only flagged in Redis for an hour, so every scheduled run kept failing with a generic error logged at ERROR three times, and the provider was asked again each hour, indefinitely. - account gains refresh_revoked_at/_code/_token_hash (expand-only, nullable). The record holds only while its hash fingerprints the stored refresh token, so a writer storing a new chain supersedes it; every reconnect path also clears it explicitly, since a provider can reauthorize without issuing a new refresh token. - The record is written only on rows still holding the rejected token, so a refresh that lost a race to a newer chain cannot mark the live credential revoked. Revocations no longer use the Redis dead flag, which now holds only app-registration faults. - A recorded revocation answers without the provider; one re-probe a day lets rejections that clear without a reconnect recover. A successful refresh clears it; a re-probe that fails for a passing reason keeps it. - refreshTokenIfNeeded throws CredentialRevokedError; token resolution returns code OAUTH_CREDENTIAL_REVOKED (POST and GET) and logs it once at WARN; the executor, tool and connector sites no longer log it at ERROR.
- Record a revocation only when the row's updated_at has not moved since the leader read it, so a reconnect that keeps the same refresh token while a rejected refresh is in flight is not undone. - Reread the row under the refresh lock, so a revocation another process just recorded is honored instead of calling the provider again. - A re-probe whose transient failure meets a moved chain reports no revocation. - Slack fan-out clears the record only for a chain the provider just issued, not when re-spreading a stored one. - A reconnect whose cleanup fails now fails the callback. - Revocation rejections log at WARN in oauth.ts too; the pure refresh error classifiers move to refresh-error-codes.ts so that client-reachable module can use them. - The connector path leaves the revocation log to executeSync.
…rocess check without Redis
…ith no Redis on the sign-in path
|
@cubic-dev-ai review this PR |
8e250cb to
a530185
Compare
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 28 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Turn on auto-fix | Re-trigger cubic
Summary
invalid_grantand kin), the refresh path only set a 1-hour Redis flag. Each scheduled run then failed with a generic error, which was logged at ERROR three times:OAuthTokenResolution"Failed to refresh access token",ExecutorCredentialToken"Credential token resolution failed", andTools"Error fetching access token for ". Every time the flag lapsed, the provider was asked again, roughly once an hour, with no end.accountgetsrefresh_revoked_at/refresh_revoked_code/refresh_revoked_token_hash.account.update.afteron callback paths).updated_athasn't moved since the leader reread the row under the refresh lock, after the existing chain-moved checks. A refresh that lost a race to a newer chain, or to a reconnect that keeps the same refresh token, records nothing. Revocations no longer use the Redis dead flag; it now holds only app-registration faults (invalid_clientetc.), which stay ERROR.refreshTokenIfNeededthrowsCredentialRevokedError. Token resolution returnscode: OAUTH_CREDENTIAL_REVOKED(POST and GET/api/auth/oauth/token) with a "reconnect" message, logged once at WARN with credential id, provider and error code.executeSync, which already logs when it unschedules the connector.oauth.tslogs revocation-class provider rejections at WARN. Its pure classifiers moved fromterminal-errors.ts, which is Redis-backed, torefresh-error-codes.ts, so this client-reachable module can import them.getCredentialTerminalRefreshErrorreads the durable record, so connectors keep unscheduling revoked credentials after any Redis TTL.0405_oauth_refresh_revoked: three nullableADD COLUMNs with no default. Expand-only and metadata-only; the deployed app neither reads nor writes these columns.What to watch after deploy
ExecutorCredentialToken/OAuthTokenResolution/Tools) becomes a singleOAuthTokenResolutionWARN, "OAuth credential revoked by provider; reconnect required", carryingcredentialId,providerIdanderrorCode.OAuthCredentialServiceWARN, "Provider revoked the refresh token; recorded until reconnect".Type of Change
Testing
lib/oauth/credential-revocation.integration.ts: 13 tests against real Postgres and Redis with a fixture token endpoint. It covers:migrateandpushprovisioning (13/13), along withcheck:migrations, lint, type-check, audits and the unit suites. Locally, before the review rounds:bun run lint,bun run type-check,bun run check:audits,bun run check:migrations,drizzle-kit generate(no changes); the affected unit suites (lib/oauth,lib/credentials,executor/utils,lib/knowledge/connectors,app/api/auth/oauth,lib/auth,tools/index.test.ts); andbun run test:integrationfor the new suite plusshopify.integration.ts.Checklist
test-auditauthoring gate)