From 65bae58cfe405eebdab6b0955840f893ba310bba Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:04:53 -0800 Subject: [PATCH] Reverse proxy fix --- Support/DOCKER-OAUTH-FIX.md | 64 ++++++++-- web_server.py | 240 +++++++++++++++++++++++------------- 2 files changed, 213 insertions(+), 91 deletions(-) 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''' - - - - - -

{page_title}

-

Step 1: {step_1_text}

-

{auth_url}

-
-

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''' + + + + + +

🔐 Spotify Authentication

+

Click the link below to authenticate with Spotify:

+

Authenticate with Spotify

+
+ Redirect URI: {configured_uri}
+ After authorizing, Spotify will redirect back automatically. Make sure this URL matches your Spotify Dashboard redirect 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''' + + + + + +

🔐 Spotify Authentication (Remote/Docker)

+ +
+ Using a reverse proxy? Your redirect URI is set to {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}/callback
+ Then update the same URI in your Spotify Dashboard. + This avoids the need for manual URL editing below. +
+ +

Step 1: Click the link below to authenticate with Spotify

+

{auth_url}

+
+

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'

🔐 Spotify Authentication

Click the link below to authenticate:

{auth_url}

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"

Spotify Authentication Failed

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 Authentication Failed

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"

Spotify Authentication Failed

{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'

Spotify Authentication Failed

{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 Authentication Failed

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'

Spotify Authentication Failed

{error}

'.encode()) + msg = ( + '

Spotify Authentication Failed

' + '

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.

' + ) + self.wfile.write(msg.encode()) def log_message(self, format, *args): pass # Suppress server logs