Reverse proxy fix

pull/253/head
Broque Thomas 2 months ago
parent d9aa8303a7
commit 65bae58cfe

@ -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
- ✅ 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

@ -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'''
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }}
code {{ background: #f0f0f0; padding: 10px; display: block; margin: 10px 0; }}
.highlight {{ background: #e8f5e9; }}
.copy-btn {{
background: #1DB954;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
margin-left: 10px;
}}
.copy-btn:hover {{ background: #1ed760; }}
.copied {{ background: #4CAF50 !important; }}
</style>
</head>
<body>
<h1>{page_title}</h1>
<p><strong>Step 1:</strong> {step_1_text}</p>
<p><a href="{auth_url}" target="_blank" style="font-size: 18px; color: #1DB954;">{auth_url}</a></p>
<hr>
<p><strong>Step 2:</strong> After authorizing, you'll see a blank page. The URL will look like:</p>
<code>http://127.0.0.1:8888/callback?code=...</code>
<p><strong>Step 3:</strong> Change <code style="display: inline; background: #ffe6e6; padding: 2px 6px;">127.0.0.1</code> to <code style="display: inline; background: #e8f5e9; padding: 2px 6px;">{host}</code> and press Enter:
<button class="copy-btn" onclick="copyIP()">Copy IP</button>
</p>
<code class="highlight">http://{host}:8888/callback?code=...</code>
<p>Authentication will then complete!</p>
<script>
function copyIP() {{
navigator.clipboard.writeText('{host}').then(() => {{
const btn = event.target;
btn.textContent = '✓ Copied!';
btn.classList.add('copied');
setTimeout(() => {{
btn.textContent = 'Copy IP';
btn.classList.remove('copied');
}}, 2000);
}});
}}
</script>
</body>
</html>
'''
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'''
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }}
code {{ background: #f0f0f0; padding: 4px 8px; border-radius: 3px; }}
.info {{ background: #e3f2fd; border-left: 4px solid #2196F3; padding: 12px 16px; margin: 16px 0; border-radius: 4px; }}
</style>
</head>
<body>
<h1>🔐 Spotify Authentication</h1>
<p>Click the link below to authenticate with Spotify:</p>
<p><a href="{auth_url}" target="_blank" style="font-size: 18px; color: #1DB954;">Authenticate with Spotify</a></p>
<div class="info">
<strong>Redirect URI:</strong> <code>{configured_uri}</code><br>
<small>After authorizing, Spotify will redirect back automatically. Make sure this URL matches your Spotify Dashboard redirect URI.</small>
</div>
<p>After authentication completes, you can close this window and return to SoulSync.</p>
</body>
</html>
'''
else:
# redirect_uri still points to port 8888 — show manual steps AND suggest switching
return f'''
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }}
code {{ background: #f0f0f0; padding: 10px; display: block; margin: 10px 0; }}
.highlight {{ background: #e8f5e9; }}
.warning {{ background: #fff3e0; border-left: 4px solid #ff9800; padding: 12px 16px; margin: 16px 0; border-radius: 4px; }}
.copy-btn {{
background: #1DB954;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
margin-left: 10px;
}}
.copy-btn:hover {{ background: #1ed760; }}
.copied {{ background: #4CAF50 !important; }}
</style>
</head>
<body>
<h1>🔐 Spotify Authentication (Remote/Docker)</h1>
<div class="warning">
<strong>Using a reverse proxy?</strong> Your redirect URI is set to <code style="display:inline; padding: 2px 6px;">{configured_uri}</code>
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.:<br>
<code style="display:inline; padding: 2px 6px; background: #e8f5e9;">https://{host}/callback</code><br>
Then update the same URI in your <a href="https://developer.spotify.com/dashboard" target="_blank">Spotify Dashboard</a>.
This avoids the need for manual URL editing below.
</div>
<p><strong>Step 1:</strong> Click the link below to authenticate with Spotify</p>
<p><a href="{auth_url}" target="_blank" style="font-size: 18px; color: #1DB954;">{auth_url}</a></p>
<hr>
<p><strong>Step 2:</strong> After authorizing, you'll see a blank page. The URL will look like:</p>
<code>http://127.0.0.1:8888/callback?code=...</code>
<p><strong>Step 3:</strong> Change <code style="display: inline; background: #ffe6e6; padding: 2px 6px;">127.0.0.1</code> to <code style="display: inline; background: #e8f5e9; padding: 2px 6px;">{host}</code> and press Enter:
<button class="copy-btn" onclick="copyIP()">Copy IP</button>
</p>
<code class="highlight">http://{host}:8888/callback?code=...</code>
<p>Authentication will then complete!</p>
<script>
function copyIP() {{
navigator.clipboard.writeText('{host}').then(() => {{
const btn = event.target;
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {{
btn.textContent = 'Copy IP';
btn.classList.remove('copied');
}}, 2000);
}});
}}
</script>
</body>
</html>
'''
else:
# Local access - simple message
return f'<h1>🔐 Spotify Authentication</h1><p>Click the link below to authenticate:</p><p><a href="{auth_url}" target="_blank">{auth_url}</a></p><p>After authentication, return to the app.</p>'
@ -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"<h1>Spotify Authentication Failed</h1><p>OAuth error: {error}</p>", 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"<h1>Spotify Authentication Failed</h1><p>Spotify returned error: {error}</p>", 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"<h1>Spotify Authentication Failed</h1><p>{str(e)}</p>", 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'<h1>Spotify Authentication Failed</h1><p>{str(e)}</p>'.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'<h1>Spotify Authentication Failed</h1><p>Spotify returned error: {error}</p>'.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'<h1>Spotify Authentication Failed</h1><p>{error}</p>'.encode())
msg = (
'<h1>Spotify Authentication Failed</h1>'
'<p>The callback was received but no authorization code or error was included.</p>'
'<p><strong>If you are using a reverse proxy:</strong> Your proxy may be stripping query parameters '
'during the redirect. Try setting your Spotify redirect URI to use port 8008 instead '
'(e.g. <code>https://yourdomain.com/callback</code>) — the main app handles callbacks too.</p>'
)
self.wfile.write(msg.encode())
def log_message(self, format, *args):
pass # Suppress server logs

Loading…
Cancel
Save