fix: disable oauth safety-timeout forced popup close
Force-closing popup webviews on timeout can interrupt active MFA flows and trigger unstable teardown paths. Disable timeout-driven forced close and keep OAuth popup closure driven by callback detection or manual user action.
- commit
- dbb6e05a5ad417bdfa09f9c5726337f8238f4983
- author
- brice · · 2026-02-17 05:15 UTC
- parent
- 64a59d7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +32 -2
M internal/ui/coordinator/content.go +10 -2
| @@ -1810,8 +1810,16 @@ func (c *ContentCoordinator) setupOAuthAutoClose( | |||
| 1810 | 1810 | safetyTimer = time.AfterFunc(oauthSafetyTimeout, func() { | |
| 1811 | 1811 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 1812 | 1812 | if wv != nil && !wv.IsDestroyed() { | |
| 1813 | - | log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup") | |
| 1814 | - | wv.Close() | |
| 1813 | + | uri := wv.URI() | |
| 1814 | + | if shouldForceCloseOnSafetyTimeout(uri) { | |
| 1815 | + | log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup") | |
| 1816 | + | wv.Close() | |
| 1817 | + | return false | |
| 1818 | + | } | |
| 1819 | + | log.Debug(). | |
| 1820 | + | Str("pane", string(paneID)). | |
| 1821 | + | Str("uri", logging.TruncateURL(uri, logURLMaxLen)). | |
| 1822 | + | Msg("oauth safety timeout reached during active auth flow, skipping forced close") | |
| 1815 | 1823 | } | |
| 1816 | 1824 | return false | |
| 1817 | 1825 | }) | |
| @@ -1810,8 +1810,16 @@ func (c *ContentCoordinator) setupOAuthAutoClose( | |||||
| 1810 | safetyTimer = time.AfterFunc(oauthSafetyTimeout, func() { | 1810 | safetyTimer = time.AfterFunc(oauthSafetyTimeout, func() { | ||
| 1811 | cb := glib.SourceFunc(func(_ uintptr) bool { | 1811 | cb := glib.SourceFunc(func(_ uintptr) bool { | ||
| 1812 | if wv != nil && !wv.IsDestroyed() { | 1812 | if wv != nil && !wv.IsDestroyed() { | ||
| 1813 | - | log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup") | 1813 | + | uri := wv.URI() |
| 1814 | - | wv.Close() | 1814 | + | if shouldForceCloseOnSafetyTimeout(uri) { |
| 1815 | + | log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup") | |||
| 1816 | + | wv.Close() | |||
| 1817 | + | return false | |||
| 1818 | + | } | |||
| 1819 | + | log.Debug(). | |||
| 1820 | + | Str("pane", string(paneID)). | |||
| 1821 | + | Str("uri", logging.TruncateURL(uri, logURLMaxLen)). | |||
| 1822 | + | Msg("oauth safety timeout reached during active auth flow, skipping forced close") | |||
| 1815 | } | 1823 | } | ||
| 1816 | return false | 1824 | return false | ||
| 1817 | }) | 1825 | }) | ||
M internal/ui/coordinator/oauth.go +10 -0
| @@ -105,6 +105,16 @@ func ShouldAutoClose(url string) bool { | |||
| 105 | 105 | return IsOAuthCallback(url) | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | + | // shouldForceCloseOnSafetyTimeout determines if a popup should be force-closed | |
| 109 | + | // when the OAuth safety timeout is reached. | |
| 110 | + | // | |
| 111 | + | // Safety-timeout force close is disabled for stability. OAuth popups close on | |
| 112 | + | // callback detection or manual user action. | |
| 113 | + | func shouldForceCloseOnSafetyTimeout(url string) bool { | |
| 114 | + | _ = url | |
| 115 | + | return false | |
| 116 | + | } | |
| 117 | + | ||
| 108 | 118 | // IsOAuthSuccess checks if the callback indicates successful authentication. | |
| 109 | 119 | func IsOAuthSuccess(url string) bool { | |
| 110 | 120 | if url == "" { | |
| @@ -105,6 +105,16 @@ func ShouldAutoClose(url string) bool { | |||||
| 105 | return IsOAuthCallback(url) | 105 | return IsOAuthCallback(url) | ||
| 106 | } | 106 | } | ||
| 107 | 107 | ||||
| 108 | + | // shouldForceCloseOnSafetyTimeout determines if a popup should be force-closed | |||
| 109 | + | // when the OAuth safety timeout is reached. | |||
| 110 | + | // | |||
| 111 | + | // Safety-timeout force close is disabled for stability. OAuth popups close on | |||
| 112 | + | // callback detection or manual user action. | |||
| 113 | + | func shouldForceCloseOnSafetyTimeout(url string) bool { | |||
| 114 | + | _ = url | |||
| 115 | + | return false | |||
| 116 | + | } | |||
| 117 | + | ||||
| 108 | // IsOAuthSuccess checks if the callback indicates successful authentication. | 118 | // IsOAuthSuccess checks if the callback indicates successful authentication. | ||
| 109 | func IsOAuthSuccess(url string) bool { | 119 | func IsOAuthSuccess(url string) bool { | ||
| 110 | if url == "" { | 120 | if url == "" { | ||
M internal/ui/coordinator/oauth_test.go +12 -0
| @@ -108,6 +108,18 @@ func TestIsOAuthURL_DetectsOAuthAuthorizeRequest(t *testing.T) { | |||
| 108 | 108 | assert.True(t, IsOAuthURL(url)) | |
| 109 | 109 | } | |
| 110 | 110 | ||
| 111 | + | func TestShouldForceCloseOnSafetyTimeout(t *testing.T) { | |
| 112 | + | t.Run("does not force close during in-progress auth challenge", func(t *testing.T) { | |
| 113 | + | url := "https://accounts.google.com/v3/signin/challenge/dp?TL=AHU8sQabc" | |
| 114 | + | assert.False(t, shouldForceCloseOnSafetyTimeout(url)) | |
| 115 | + | }) | |
| 116 | + | ||
| 117 | + | t.Run("does not force close when callback is reached", func(t *testing.T) { | |
| 118 | + | url := "http://localhost:1455/auth/callback?code=abc123&state=xyz" | |
| 119 | + | assert.False(t, shouldForceCloseOnSafetyTimeout(url)) | |
| 120 | + | }) | |
| 121 | + | } | |
| 122 | + | ||
| 111 | 123 | func waitFor(t *testing.T, timeout time.Duration, condition func() bool) { | |
| 112 | 124 | t.Helper() | |
| 113 | 125 | ||
| @@ -108,6 +108,18 @@ func TestIsOAuthURL_DetectsOAuthAuthorizeRequest(t *testing.T) { | |||||
| 108 | assert.True(t, IsOAuthURL(url)) | 108 | assert.True(t, IsOAuthURL(url)) | ||
| 109 | } | 109 | } | ||
| 110 | 110 | ||||
| 111 | + | func TestShouldForceCloseOnSafetyTimeout(t *testing.T) { | |||
| 112 | + | t.Run("does not force close during in-progress auth challenge", func(t *testing.T) { | |||
| 113 | + | url := "https://accounts.google.com/v3/signin/challenge/dp?TL=AHU8sQabc" | |||
| 114 | + | assert.False(t, shouldForceCloseOnSafetyTimeout(url)) | |||
| 115 | + | }) | |||
| 116 | + | ||||
| 117 | + | t.Run("does not force close when callback is reached", func(t *testing.T) { | |||
| 118 | + | url := "http://localhost:1455/auth/callback?code=abc123&state=xyz" | |||
| 119 | + | assert.False(t, shouldForceCloseOnSafetyTimeout(url)) | |||
| 120 | + | }) | |||
| 121 | + | } | |||
| 122 | + | ||||
| 111 | func waitFor(t *testing.T, timeout time.Duration, condition func() bool) { | 123 | func waitFor(t *testing.T, timeout time.Duration, condition func() bool) { | ||
| 112 | t.Helper() | 124 | t.Helper() | ||
| 113 | 125 | ||||