diff --git a/Support/DOCKER-OAUTH-FIX.md b/Support/DOCKER-OAUTH-FIX.md index 3c5f3e32..2a405acb 100644 --- a/Support/DOCKER-OAUTH-FIX.md +++ b/Support/DOCKER-OAUTH-FIX.md @@ -49,16 +49,66 @@ If you can access SoulSync directly from the Docker host machine: - Set OAuth redirect URIs to localhost (as above) - No SSH tunnel needed -## 🔧 For Advanced Users: Reverse Proxy +## 🔧 Reverse Proxy Setup (Caddy, Nginx, Traefik) -Set up nginx/traefik with proper SSL certificates for true HTTPS support. See community guides for Docker reverse proxy setups. +If you're running SoulSync behind a reverse proxy with HTTPS, you can use the **main app port (8008)** for OAuth callbacks instead of the standalone port 8888. This is the recommended approach for reverse proxy setups. + +### Step 1: Set your redirect URI to your proxy URL + +**In SoulSync Settings:** +- Set Spotify redirect URI to: `https://yourdomain.com/callback` + +**In your Spotify Developer Dashboard:** +- Add the same redirect URI: `https://yourdomain.com/callback` + +### Step 2: Ensure your reverse proxy forwards to port 8008 + +Your reverse proxy should forward traffic to SoulSync's main port (8008). The `/callback` path is handled by the main Flask app — no need to expose port 8888. + +**Example Caddy config:** +``` +soulsync.yourdomain.com { + reverse_proxy localhost:8008 +} +``` + +**Example Nginx config:** +```nginx +server { + listen 443 ssl; + server_name soulsync.yourdomain.com; + + location / { + proxy_pass http://localhost:8008; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Step 3: Authenticate normally + +Click "Connect Spotify" in SoulSync settings. After authorizing on Spotify, you'll be redirected back through your reverse proxy automatically. + +### Important notes for reverse proxy users + +- The redirect URI **must use HTTPS** for non-localhost domains (Spotify requirement) +- The redirect URI in SoulSync settings **must exactly match** the one in your Spotify Dashboard +- Port 8888 is only needed for direct/local access — you do **not** need to expose it through your proxy +- Make sure your proxy passes query parameters through unmodified (most do by default) ## 📝 Summary -The core issue is that **Spotify requires HTTPS for non-localhost** OAuth redirects. The SSH tunnel makes remote devices appear as localhost to bypass this requirement. +The core issue is that **Spotify requires HTTPS for non-localhost** OAuth redirects. + +**Choose your approach:** +- **Reverse proxy with HTTPS**: Set redirect URI to `https://yourdomain.com/callback` (recommended for production) +- **SSH tunnel**: Makes remote devices appear as localhost — set redirect URI to `http://127.0.0.1:8888/callback` +- **Local access**: No special config needed — default `http://127.0.0.1:8888/callback` works **Key points:** -- ✅ Always use `127.0.0.1` in OAuth redirect URIs -- ✅ Use SSH tunnel when accessing from different device -- ✅ Keep tunnel open during authentication -- ✅ Works with existing Docker setup - no changes needed \ No newline at end of file +- ✅ Reverse proxy users: use `https://yourdomain.com/callback` on port 8008 +- ✅ SSH tunnel users: use `http://127.0.0.1:8888/callback` on port 8888 +- ✅ Redirect URI must match exactly in SoulSync settings AND Spotify Dashboard +- ✅ Query parameters must be preserved through the redirect chain \ No newline at end of file diff --git a/web_server.py b/web_server.py index a0c74c61..ccf0f47a 100644 --- a/web_server.py +++ b/web_server.py @@ -3334,72 +3334,111 @@ def auth_spotify(): if temp_spotify_client.sp and temp_spotify_client.sp.auth_manager: # Get the authorization URL auth_url = temp_spotify_client.sp.auth_manager.get_authorize_url() + configured_uri = config_manager.get_spotify_config().get('redirect_uri', 'http://127.0.0.1:8888/callback') + print(f"🎵 Spotify auth initiated — redirect_uri: {configured_uri}") add_activity_item("🔐", "Spotify Auth Started", "Please complete OAuth in browser", "Now") # Detect if accessing remotely host = request.host.split(':')[0] is_remote = host not in ['127.0.0.1', 'localhost'] is_docker = os.path.exists('/.dockerenv') - + # If in Docker and accessing via 127.0.0.1, recommend localhost if is_docker and host == '127.0.0.1': host = 'localhost' + # Check if the redirect_uri uses port 8008 (main app) vs 8888 (standalone) + uses_main_port = ':8008' in configured_uri or ':8888' not in configured_uri + if is_remote or is_docker: # Show instructions for remote/docker access - page_title = "🔐 Spotify Authentication (Remote/Docker)" - step_1_text = "Click the link below to authenticate with Spotify" - - return f''' - -
- - - -Step 1: {step_1_text}
- -Step 2: After authorizing, you'll see a blank page. The URL will look like:
-http://127.0.0.1:8888/callback?code=...
- Step 3: Change 127.0.0.1 to {host} and press Enter:
-
-
http://{host}:8888/callback?code=...
- Authentication will then complete!
- - - - - ''' + if uses_main_port: + # redirect_uri already points to port 8008 or a custom domain — + # callback will come through the main Flask app, no manual steps needed + return f''' + + + + + +Click the link below to authenticate with Spotify:
+ +{configured_uri}After authentication completes, you can close this window and return to SoulSync.
+ + + ''' + else: + # redirect_uri still points to port 8888 — show manual steps AND suggest switching + return f''' + + + + + +{configured_uri}
+ which uses port 8888. If you're behind a reverse proxy (Caddy, Nginx, Traefik), change the
+ redirect URI in SoulSync settings to use your proxy URL on the main port instead, e.g.:https://{host}/callbackStep 1: Click the link below to authenticate with Spotify
+ +Step 2: After authorizing, you'll see a blank page. The URL will look like:
+http://127.0.0.1:8888/callback?code=...
+ Step 3: Change 127.0.0.1 to {host} and press Enter:
+
+
http://{host}:8888/callback?code=...
+ Authentication will then complete!
+ + + + + ''' else: # Local access - simple message return f'Click the link below to authenticate:
After authentication, return to the app.
' @@ -3530,18 +3569,28 @@ def auth_tidal(): def spotify_callback(): """ Handles Spotify OAuth callback via the main Flask app (port 8008). - This allows reverse proxy users to use a redirect_uri pointing at the main app. - The dedicated HTTPServer on port 8888 continues to work for direct access. + This is the recommended callback for reverse proxy / Docker setups. + The dedicated HTTPServer on port 8888 continues to work for direct/local access. """ global spotify_client auth_code = request.args.get('code') if not auth_code: - error = request.args.get('error', 'Unknown error') - if 'error' not in request.args: - # Spurious request (e.g., healthcheck) - ignore silently - return '', 204 - return f"OAuth error: {error}
", 400 + error = request.args.get('error') + if error: + print(f"🔴 Spotify OAuth error on port 8008: Spotify returned error: {error}") + add_activity_item("❌", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now") + return f"Spotify returned error: {error}
", 400 + + # No code AND no error — check if query params were stripped + if request.args: + print(f"🔴 Spotify callback on port 8008 received unexpected params: {dict(request.args)}") + else: + # Completely empty — likely a healthcheck or spurious request + pass + return '', 204 + + print(f"🎵 Spotify callback received on port 8008 with authorization code") try: from core.spotify_client import SpotifyClient @@ -3549,10 +3598,13 @@ def spotify_callback(): from config.settings import config_manager config = config_manager.get_spotify_config() + configured_uri = config.get('redirect_uri', "http://127.0.0.1:8888/callback") + print(f"🎵 Using redirect_uri for token exchange: {configured_uri}") + auth_manager = SpotifyOAuth( client_id=config['client_id'], client_secret=config['client_secret'], - redirect_uri=config.get('redirect_uri', "http://127.0.0.1:8888/callback"), + redirect_uri=configured_uri, scope="user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", cache_path='config/.spotify_cache' ) @@ -3571,7 +3623,7 @@ def spotify_callback(): else: raise Exception("Failed to exchange authorization code for access token") except Exception as e: - print(f"🔴 Spotify OAuth callback error: {e}") + print(f"🔴 Spotify OAuth callback error on port 8008: {e}") add_activity_item("❌", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now") return f"{str(e)}
", 400 @@ -27806,38 +27858,49 @@ def start_oauth_callback_servers(): from http.server import HTTPServer, BaseHTTPRequestHandler import urllib.parse - # Spotify callback server + # Spotify callback server (port 8888 — for direct/local access only) class SpotifyCallbackHandler(BaseHTTPRequestHandler): def do_GET(self): - print(f"🎵 Spotify callback received: {self.path}") parsed_url = urllib.parse.urlparse(self.path) + + # Only process requests to /callback — ignore everything else + if parsed_url.path != '/callback': + self.send_response(404) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(b'Not found. Spotify callback is at /callback') + return + query_params = urllib.parse.parse_qs(parsed_url.query) - + print(f"🎵 Spotify callback received on port 8888: {self.path}") + if 'code' in query_params: auth_code = query_params['code'][0] print(f"🎵 Received Spotify authorization code: {auth_code[:10]}...") - + # Manually trigger the token exchange using spotipy's auth manager try: from core.spotify_client import SpotifyClient from spotipy.oauth2 import SpotifyOAuth from config.settings import config_manager - + # Get Spotify config config = config_manager.get_spotify_config() - + configured_uri = config.get('redirect_uri', "http://127.0.0.1:8888/callback") + print(f"🎵 Using redirect_uri for token exchange: {configured_uri}") + # Create auth manager and exchange code for token auth_manager = SpotifyOAuth( client_id=config['client_id'], client_secret=config['client_secret'], - redirect_uri=config.get('redirect_uri', "http://127.0.0.1:8888/callback"), + redirect_uri=configured_uri, scope="user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", cache_path='config/.spotify_cache' ) - + # Extract the authorization code and exchange it for tokens token_info = auth_manager.get_access_token(auth_code, as_dict=True) - + if token_info: # Reinitialize the global client with new tokens global spotify_client @@ -27862,22 +27925,31 @@ def start_oauth_callback_servers(): self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(f'{str(e)}
'.encode()) + elif 'error' in query_params: + error = query_params['error'][0] + print(f"🔴 Spotify OAuth error returned by Spotify: {error}") + print(f"🔴 Full callback URL: {self.path}") + add_activity_item("❌", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now") + self.send_response(400) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(f'Spotify returned error: {error}
'.encode()) else: - error = query_params.get('error', ['Unknown error'])[0] - print(f"🔴 Spotify OAuth error: {error}") - print(f"🔴 Full Spotify callback URL: {self.path}") - print(f"🔴 All query params: {query_params}") - - # Only show error toast if it's not just a spurious request - if 'error' in query_params: - add_activity_item("❌", "Spotify Auth Failed", f"OAuth error: {error}", "Now") - else: - print("🔴 Spurious Spotify callback without code or error - ignoring") - + # No code AND no error — callback was hit without OAuth params + print(f"🔴 Spotify callback received without OAuth parameters (no code or error)") + print(f"🔴 Path: {self.path} | Query params: {query_params}") + print(f"🔴 This usually means the redirect lost its query parameters (reverse proxy issue)") self.send_response(400) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(f'{error}
'.encode()) + msg = ( + 'The callback was received but no authorization code or error was included.
' + 'If you are using a reverse proxy: Your proxy may be stripping query parameters '
+ 'during the redirect. Try setting your Spotify redirect URI to use port 8008 instead '
+ '(e.g. https://yourdomain.com/callback) — the main app handles callbacks too.