diff --git a/app/internal_packages/onboarding/lib/oauth-signin-page.tsx b/app/internal_packages/onboarding/lib/oauth-signin-page.tsx index 7a3ce87d6b..48600407c5 100644 --- a/app/internal_packages/onboarding/lib/oauth-signin-page.tsx +++ b/app/internal_packages/onboarding/lib/oauth-signin-page.tsx @@ -55,6 +55,7 @@ export default class OAuthSignInPage extends React.Component< _startTimer: NodeJS.Timeout; _warnTimer: NodeJS.Timeout; _mounted = false; + _lastCodeReceived: string | null = null; state: OAuthSignInPageState = { authStage: 'initial', @@ -79,7 +80,20 @@ export default class OAuthSignInPage extends React.Component< if (!this._mounted) return; const code = extractOAuthCodeFromUrl(request.url); if (code) { - this._onReceivedCode(code); + // Browsers, security software, and link-preview tools can hit this URL more + // than once for the *same* code (retries, prefetching, back/forward replay). + // Authorization codes are single-use, so re-submitting an identical code just + // fails with an "invalid_grant" error from the provider — ignore repeats of a + // code we've already started exchanging. But the user can also legitimately + // go back in their browser and complete sign-in again with a different + // account, producing a genuinely new code; only ignore that if we're already + // mid-exchange (or have already succeeded) for the previous one. + const isNewCode = code !== this._lastCodeReceived; + const isBusy = this.state.authStage === 'buildingAccount' || this.state.authStage === 'accountSuccess'; + if (isNewCode && !isBusy) { + this._lastCodeReceived = code; + this._onReceivedCode(code); + } response.writeHead(302, { Location: 'https://id.getmailspring.com/oauth/finished' }); response.end(); } else {