Some fixes for the oauth redirect page
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 30 Nov 2025 13:15:21 +0000 (14:15 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 30 Nov 2025 13:15:21 +0000 (14:15 +0100)
music_assistant/controllers/webserver/controller.py
music_assistant/helpers/resources/oauth_callback.html

index ad62b83e916e6326ff084f65539e58066f5c45e9..7d386cb7bd9eb6441a05e244c015b028d5f53dfe 100644 (file)
@@ -820,7 +820,7 @@ class WebserverController(CoreController):
             self.logger.exception("Error during OAuth authorization")
             return web.json_response({"error": "Authorization failed"}, status=500)
 
-    async def _handle_auth_callback(self, request: web.Request) -> web.Response:
+    async def _handle_auth_callback(self, request: web.Request) -> web.Response:  # noqa: PLR0915
         """Handle OAuth callback."""
         try:
             code = request.query.get("code")
@@ -852,27 +852,20 @@ class WebserverController(CoreController):
             device_name = f"OAuth ({provider_id})"
             token = await self.auth.create_token(auth_result.user, device_name)
 
-            # Check if this is a remote client OAuth flow
             if auth_result.return_url and auth_result.return_url.startswith(
                 "urn:ietf:wg:oauth:2.0:oob:auto:"
             ):
-                # Extract session ID from return URL
                 session_id = auth_result.return_url.split(":")[-1]
-                # Store token in pending sessions
                 if session_id in self.auth._pending_oauth_sessions:
                     self.auth._pending_oauth_sessions[session_id] = token
-                    # Show success page for remote auth
-                    success_html = """
-                    <html>
-                    <head><title>Authentication Successful</title></head>
-                    <body style="font-family: Arial, sans-serif; text-align: center;
-                                 padding: 50px;">
-                        <h1 style="color: #4CAF50;">✓ Authentication Successful</h1>
-                        <p>You have successfully authenticated with Music Assistant.</p>
-                        <p>You can now close this window and return to your application.</p>
-                    </body>
-                    </html>
-                    """
+                    oauth_callback_html_path = str(RESOURCES_DIR.joinpath("oauth_callback.html"))
+                    async with aiofiles.open(oauth_callback_html_path) as f:
+                        success_html = await f.read()
+
+                    success_html = success_html.replace("{TOKEN}", token)
+                    success_html = success_html.replace("{REDIRECT_URL}", "about:blank")
+                    success_html = success_html.replace("{REQUIRES_CONSENT}", "false")
+
                     return web.Response(text=success_html, content_type="text/html")
 
             # Determine redirect URL (use return_url from OAuth flow or default to root)
index 88933578b52a69319591eed8e07c6f8c91d3bb75..ef030e90cd4f4299e627a20210e9a1998fa19685 100644 (file)
             color: var(--text-secondary);
             font-size: 15px;
         }
+
+        .close-button {
+            display: none;
+            margin-top: 24px;
+            padding: 12px 24px;
+            background: var(--primary);
+            color: white;
+            border: none;
+            border-radius: 10px;
+            font-size: 15px;
+            font-weight: 600;
+            cursor: pointer;
+            transition: all 0.2s ease;
+        }
+
+        .close-button:hover {
+            filter: brightness(1.1);
+            transform: translateY(-1px);
+        }
+
+        .close-button:active {
+            transform: translateY(0);
+        }
+
+        .close-button.show {
+            display: inline-block;
+        }
     </style>
 </head>
 <body>
     <div class="callback-container">
         <h1 id="status">Login Successful!</h1>
         <p id="message">Redirecting...</p>
+        <button class="close-button" id="closeButton" onclick="handleManualClose()">Close Window</button>
     </div>
     <script>
         const statusEl = document.getElementById('status');
         const redirectUrl = '{REDIRECT_URL}';
         const requiresConsent = {REQUIRES_CONSENT};
 
-        // Detect if running in PWA/standalone mode
-        // iOS Safari sets window.navigator.standalone
-        // Other browsers support display-mode media query
         const isPWA = (window.navigator.standalone === true) ||
                       window.matchMedia('(display-mode: standalone)').matches ||
                       window.matchMedia('(display-mode: minimal-ui)').matches;
 
-        console.log('OAuth callback - isPWA:', isPWA, 'opener:', window.opener !== null);
-
-        // Validate URL to prevent XSS attacks
-        // Note: Server-side validation is the primary security layer
         function isValidRedirectUrl(url) {
             if (!url) return false;
             try {
                 const parsed = new URL(url, window.location.origin);
-                // Allow http, https, and custom mobile app schemes
                 const allowedProtocols = ['http:', 'https:', 'musicassistant:'];
                 return allowedProtocols.includes(parsed.protocol);
             } catch {
             }
         }
 
+        function attemptAutoClose() {
+            window.close();
+            if (window.self !== window.top) {
+                window.self.close();
+            }
+            if (window.opener) {
+                try {
+                    window.opener.focus();
+                    window.close();
+                } catch (e) {
+                    // Silently fail
+                }
+            }
+        }
+
+        function showManualCloseButton() {
+            const closeButton = document.getElementById('closeButton');
+            if (closeButton) {
+                closeButton.classList.add('show');
+            }
+            messageEl.textContent = 'Authentication successful! You can close this window.';
+        }
+
+        function handleManualClose() {
+            attemptAutoClose();
+            setTimeout(() => {
+                if (window.opener && !window.opener.closed) {
+                    window.blur();
+                }
+                messageEl.textContent = 'Please close this window manually.';
+                statusEl.textContent = 'Ready to Close';
+            }, 500);
+        }
+
         function performRedirect() {
             const isPopup = window.opener !== null;
+            const isRemoteAuth = redirectUrl === 'about:blank';
+
+            if (isRemoteAuth) {
+                statusEl.textContent = 'Authentication Successful';
+                messageEl.textContent = 'Closing window...';
+                setTimeout(() => {
+                    attemptAutoClose();
+                    setTimeout(() => {
+                        showManualCloseButton();
+                    }, 300);
+                }, 100);
+                return;
+            }
 
-            // PWA mode: Always use direct redirect since popups don't work properly
-            // The OAuth flow on iOS PWAs opens in Safari, then redirects back to PWA
-            // This means window.opener is always null and localStorage is not shared
+            // PWA mode: OAuth flow on iOS PWAs opens in Safari, then redirects back to PWA
+            // window.opener is null and localStorage is not shared between contexts
             if (isPWA) {
                 statusEl.textContent = 'Login Complete!';
                 messageEl.textContent = 'Returning to app...';
-
-                // Don't store token in localStorage here - it's already in the redirect URL
-                // The frontend will extract it from the URL query parameter
-                // PWAs on iOS may not share localStorage between Safari and the PWA context
-
-                console.log('PWA mode - redirecting to:', redirectUrl);
-
-                // Navigate to redirect URL (token is already in URL as query param 'code')
                 if (isValidRedirectUrl(redirectUrl)) {
                     window.location.href = redirectUrl;
                 } else {
-                    console.warn('Invalid redirect URL, using fallback:', redirectUrl);
                     window.location.href = '/';
                 }
                 return;
             }
 
             if (isPopup) {
-                // Popup mode - send token to parent and close
                 statusEl.textContent = 'Login Complete!';
                 messageEl.textContent = 'Closing popup...';
 
                             token: token,
                             redirectUrl: redirectUrl
                         }, window.location.origin);
-                        console.log('Successfully sent postMessage to opener');
                     } catch (e) {
-                        console.error('Failed to send postMessage:', e);
+                        // Silently fail
                     }
                 }
 
-                // Modern browsers allow window.close() for windows opened via window.open()
-                // Try to close immediately after sending the message
                 setTimeout(() => {
-                    // First try to close the window
-                    window.close();
-
-                    // Schedule a check to see if close was successful
-                    // If the window is still open after 300ms, it means close() was blocked
+                    attemptAutoClose();
                     setTimeout(() => {
-                        // This will only execute if the window wasn't closed
-                        messageEl.textContent = 'Authentication successful! You can close this window.';
+                        showManualCloseButton();
                     }, 300);
                 }, 100);
             } else {
-                // Same window mode - redirect directly
+                statusEl.textContent = 'Authentication Successful';
+                messageEl.textContent = 'Redirecting...';
                 localStorage.setItem('auth_token', token);
+
                 if (isValidRedirectUrl(redirectUrl)) {
-                    window.location.href = redirectUrl;
+                    setTimeout(() => {
+                        window.location.href = redirectUrl;
+                    }, 500);
                 } else {
-                    window.location.href = '/';
+                    setTimeout(() => {
+                        messageEl.textContent = 'Redirect failed. Please close this window.';
+                        showManualCloseButton();
+                    }, 1000);
                 }
+
+                setTimeout(() => {
+                    showManualCloseButton();
+                }, 2000);
             }
         }
 
             window.location.href = '/';
         }
 
-        // Check if consent is required
         if (requiresConsent) {
-            // Show consent banner with domain
             try {
                 const parsed = new URL(redirectUrl);
                 document.getElementById('redirectDomain').textContent = parsed.origin;
             statusEl.textContent = 'Authorization Required';
             messageEl.textContent = 'Please review the authorization request above.';
         } else {
-            // Trusted domain, proceed with redirect
             performRedirect();
         }
     </script>