fix(oauth): stop treating a foreign window.opener as a bind flow (#6425)
* fix(oauth): stop treating a foreign window.opener as a bind flow The /oauth/:provider callback decided between an account bind and a plain login with `window.opener ? 'bind' : 'login'`. Any tab opened from an external link (target="_blank", Slack, mail clients, another site) carries a live opener, and that opener survives the cross-origin round trip to the identity provider. Such a login callback was therefore misread as a bind: it posted a handshake to a window that speaks no such protocol, showed the "binding your account" screen, and hung until the 30s deadline fired with "OAuth binding timed out" — while the backend was never called at all. Reproduced against a real Keycloak round trip: a tab opened via window.open still reports window.opener !== null on the callback, so mode resolved to 'bind' for an ordinary OIDC login. A bind now requires positive proof: the popup we open for it is same-origin (about:blank) before being sent to the provider, so we stamp its own sessionStorage. The stamp rides through the provider round trip and is scoped to that popup alone, so a login tab can never carry it. Ambiguity resolves to 'login', which is the recoverable direction. Affects every provider sharing this callback (OIDC, GitHub, Discord, LinuxDO, custom). * fix(oauth): harden bind popup detection
This commit is contained in:
@@ -38,6 +38,10 @@ import {
|
||||
postTelegramBindResult,
|
||||
startOAuthBindResponseDeadline,
|
||||
} from '@/features/auth/lib/oauth-bind-window'
|
||||
import {
|
||||
getOAuthSessionStorage,
|
||||
resolveOAuthCallbackMode,
|
||||
} from '@/features/auth/lib/oauth-callback-mode'
|
||||
import { api, applyAuthBundle, isAuthBundle } from '@/lib/api'
|
||||
import { getServerErrorMessageKey } from '@/lib/server-error-message'
|
||||
|
||||
@@ -68,14 +72,25 @@ function OAuthCallback() {
|
||||
flow_token?: string
|
||||
error_code?: string
|
||||
}
|
||||
const mode: 'login' | 'bind' =
|
||||
typeof window !== 'undefined' && window.opener ? 'bind' : 'login'
|
||||
const callbackState = search.state ?? ''
|
||||
const isTelegramBindCallback =
|
||||
provider === 'telegram' &&
|
||||
(search.telegram_bind === 'success' || search.telegram_bind === 'error')
|
||||
let mode: 'login' | 'bind' = 'login'
|
||||
if (isTelegramBindCallback) {
|
||||
mode = 'bind'
|
||||
} else if (typeof window !== 'undefined') {
|
||||
mode = resolveOAuthCallbackMode(provider, callbackState, {
|
||||
opener: window.opener,
|
||||
storage: getOAuthSessionStorage(window),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const code = search.code ?? ''
|
||||
const state = search.state ?? ''
|
||||
const state = callbackState
|
||||
const telegramCallback =
|
||||
provider === 'telegram'
|
||||
? parseTelegramBindCallback({
|
||||
@@ -212,6 +227,7 @@ function OAuthCallback() {
|
||||
safeNavigate('/sign-in', '/sign-in')
|
||||
})()
|
||||
}, [
|
||||
callbackState,
|
||||
mode,
|
||||
navigate,
|
||||
provider,
|
||||
@@ -221,7 +237,6 @@ function OAuthCallback() {
|
||||
search.error_description,
|
||||
search.flow_token,
|
||||
search.redirect,
|
||||
search.state,
|
||||
search.telegram_bind,
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user