feat: revoke upstream OAuth2 grant on account delete via ?revoke=true#595
Open
bgeihsgt wants to merge 1 commit into
Open
feat: revoke upstream OAuth2 grant on account delete via ?revoke=true#595bgeihsgt wants to merge 1 commit into
bgeihsgt wants to merge 1 commit into
Conversation
DELETE /v1/account/{account} now accepts a 'revoke' query parameter.
When revoke=true and the account is OAuth2-backed with a provider that
implements revokeToken (currently Gmail), EmailEngine posts the active
access token to the provider's revoke endpoint before tearing down the
account state.
The revoke step is best-effort. Failures (network errors, non-OK
responses, missing OAuth app, providers without revokeToken support)
are logged and do not block deletion, so the externally visible
behavior of DELETE remains unchanged when the upstream provider
refuses or cannot honor the call. Default (revoke=false) preserves
the prior no-revoke behavior for existing integrations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in
revokequery parameter toDELETE /v1/account/{account}. Whenrevoke=true, EmailEngine attempts to revoke the upstream OAuth2 grant at the provider before tearing down the account's local state, by calling the existingrevokeTokenmethod on the OAuth2 client returned byoauth2Apps.getClient(provider).Default behavior is unchanged (
revoke=false).Motivation
Right now,
DELETE /v1/account/{account}clears EmailEngine-side state but leaves the user's OAuth grant active at the provider. From the end user's perspective, "disconnecting" their mailbox in a downstream application leaves access still listed atmyaccount.google.com/permissions, and reconnecting re-uses the existing grant rather than prompting fresh consent — which is confusing and a soft security wart.The pieces to fix this already exist inside EmailEngine:
GmailOauth.revokeToken(lib/oauth/gmail.js:748) is implemented, well-tested intest/oauth-integration-test.js, and currently wired to one call site only (the OAuth callback path that handles insufficient granular consent scopes). This PR wires the same primitive into the account-delete path behind an explicit opt-in flag.The alternative is to enable
enableOAuthTokensApiso downstream apps can pull the access token themselves and revoke it client-side. That works, but it exposes raw OAuth tokens over the API surface to do something EmailEngine could just do itself, and many operators (myself included) don't want to flip that setting on. A server-side opt-in keeps the token inside EmailEngine.Behavior
revoke=false(default): unchanged — DELETE works exactly as before.revoke=true+ OAuth2 provider withrevokeToken(Gmail): the access token fromaccountData.oauth2.accessTokenis POSTed to the provider's revoke endpoint via the existingoauthClient.revokeToken(...). Per Google's docs, revoking either the access or refresh token invalidates the entire grant.revoke=true+ OAuth2 provider withoutrevokeToken(Outlook, MailRu): no-op — thetypeof oauthClient.revokeToken === 'function'guard skips silently.revoke=true+ non-OAuth account (IMAP/SMTP password): no-op — theaccountData.oauth2.providerguard skips silently.this.logger.warn, and does not block deletion. The existing best-effort semantics insiderevokeTokenitself are preserved.Implementation
Two files, ~26 lines:
lib/account.jsdelete(opts)— accepts an optional{ revoke }flag; when set and the account is OAuth2-backed, resolves the OAuth client viaoauth2Apps.getClientand callsrevokeTokeninside atry/catch.lib/api-routes/account-routes.jsDELETE handler — adds therevokequery param with the sameJoi.boolean().truthy('Y','true','1').falsy('N','false',0).default(false)pattern used elsewhere in this file (e.g.,reconnect,sync,flush), and passes it through toaccountObject.delete({ revoke }).Tests
The underlying
revokeTokenis already covered by three tests intest/oauth-integration-test.js:This PR's wiring is straightforward best-effort plumbing on top of that, so I haven't added new tests. Happy to add a focused test if you'd like — most natural place would be a
node:testblock exercisingAccount.delete({ revoke: true })against a stub OAuth client, but I wanted to keep the diff minimal for the first round of review.Notes
revokeTokenmethod inlib/oauth/outlook.js. Microsoft's v2 OAuth doesn't have a meaningful revoke endpoint for delegated permissions anyway, so this PR intentionally does not change Outlook behavior.Per
.github/contributing.md, I confirm I am submitting this change for assignment to Postal Systems OÜ.