Skip to content

fix: revoke OAuth refresh tokens on session lifecycle#83

Merged
FrkAk merged 9 commits into
mainfrom
fix/mymr-159-revoke-oauth-refresh-tokens-on
May 17, 2026
Merged

fix: revoke OAuth refresh tokens on session lifecycle#83
FrkAk merged 9 commits into
mainfrom
fix/mymr-159-revoke-oauth-refresh-tokens-on

Conversation

@FrkAk
Copy link
Copy Markdown
Owner

@FrkAk FrkAk commented May 17, 2026

Summary

Task Reference: [MYMR-159]

Tightens the MCP OAuth provider so a leaked refresh token cannot outlive the password that minted it, and gives users a single-click "Revoke all" in the Agents tab.

Changes

  • lib/auth.ts — cascade hook: databaseHooks.account.update.after, filtered by providerId === "credential", calls clearUserOAuthArtifacts(userId) whenever a password mutation lands. Covers /change-password, the update-user password path, admin password reset, and revokeSessionsOnPasswordReset. Wrapped in try / catch + console.error to match organizationHooks.afterRemoveMember — BA fires databaseHooks post-commit, so a thrown error would not roll back the parent write.
  • lib/auth.tsoauthProvider config:
    • accessTokenExpiresIn: 60 * 60 (1h). Pinned explicitly so a future plugin default change is intentional.
    • refreshTokenExpiresIn: 60 * 60 * 24 * 7 (7d). Aligned with session.expiresIn instead of the plugin's 30-day default.
    • clientRegistrationAllowedScopes: ["openid", "profile", "email", "offline_access"]. Defensive allowlist — functional no-op today; prevents a future scope addition from silently widening DCR.
  • lib/data/oauth-session.ts — new clearUserOAuthArtifacts(userId) hard-deletes every oauthAccessToken and oauthRefreshToken row for a user inside a single serviceRoleDb transaction. Idempotent. Reused by both the cascade hook and the bulk-revoke action.
  • lib/actions/oauth-session.ts — new revokeAllOAuthSessionsAction. Mirrors revokeOAuthSessionAction's shape (requireSessioncheckActionRateLimit → mutate → revalidatePath("/settings")). Rate-limited at perUserMax: 3 / perIpMax: 10 / windowSeconds: 60.
  • app/settings/_components/AgentsTab.tsx — adds a "Revoke all" InlineConfirm row, hidden when no sessions exist. InlineConfirm already wraps onConfirm in useTransition and disables the confirm button with a loading indicator. Subhead copy updated to "Sessions authorized to run via MCP. Changing your password revokes every session automatically." Brand order is now Claude Code → Codex → Gemini → Cursor.
  • docker/init-pg-cron.sql (new) — nightly 0 3 * * * (UTC) janitor that purges revoked or expired rows from oauthRefreshToken and expired rows from oauthAccessToken. Belt-and-braces alongside the cascade hook. Requires the pg_cron extension (Neon console toggle, or self-host superuser). Not wired into tests/setup/migrate.ts — the testcontainer image ships without pg_cron and the suite does not need it. docker/init-auth.sql has a pointer comment so operators see the follow-up step.

Why the account.update.after filter is safe

The providerId === "credential" filter is sufficient because every credential-row mutation in BA 1.6.11 is a password change (updatePassword and updateAccount({ password }) in api/routes/password.mjs, update-user.mjs, admin/, email-otp/, phone-number/). Social provider sign-ins update social account rows, which the filter excludes. Plain email/password sign-in does not touch the account row at all, so logins do not trigger the cascade.

Why hard delete and not soft revoke

listActiveOAuthSessions already filters on isNull(revoked), so hard-deleting is observationally equivalent and reclaims space. The single-session revoke keeps the soft-revoke audit-row semantics; bulk revoke and the cascade hard-delete.

Why no referenceId filter on the cascade

consentReferenceId: () => undefined (lib/auth.ts) means refresh-token rows are written with referenceId = null today. A future change that populates referenceId would still want a full-user cascade — filtering would mask future regressions.

Type of change

  • Bug fix
  • New feature (revoke-all action + UI, pg_cron janitor)
  • Refactor / cleanup
  • Documentation

Testing

  • Tested locally with `bun run dev`
  • Linting passes (`bun run lint`)
  • Typecheck passes (`bun run typecheck`)
  • Format clean (`bun run format:check`)
  • Full test suite passes (`bun test` — 457 pass, 0 fail)

@FrkAk FrkAk requested review from ZeyNor and ulascanzorer as code owners May 17, 2026 14:49
@FrkAk FrkAk changed the title fix: revoke OAuth refresh tokens on session and password lifecycle fix: harden MCP OAuth lifecycle with Strategy C May 17, 2026
@FrkAk FrkAk self-assigned this May 17, 2026
@FrkAk FrkAk changed the title fix: harden MCP OAuth lifecycle with Strategy C fix: cascade MCP OAuth revoke to password change May 17, 2026
@FrkAk FrkAk changed the title fix: cascade MCP OAuth revoke to password change fix: revoke OAuth refresh tokens on session lifecycle May 17, 2026
@FrkAk FrkAk merged commit 4f467b1 into main May 17, 2026
4 checks passed
@FrkAk FrkAk deleted the fix/mymr-159-revoke-oauth-refresh-tokens-on branch May 17, 2026 16:36
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