diff --git a/.env.oauth.production b/.env.oauth.production new file mode 100644 index 0000000..22eeb1f --- /dev/null +++ b/.env.oauth.production @@ -0,0 +1,20 @@ +# Standard OAuth redirect URIs +SPOTIFY_REDIRECT_URI=https://qb.kaufdeinquiz.com/auth/spotify/callback +GOOGLE_REDIRECT_URI=https://qb.kaufdeinquiz.com/users/login/google/callback +AUTHENTIK_REDIRECT_URI=https://qb.kaufdeinquiz.com/users/login/authentik/callback +DROPBOX_REDIRECT_URI=https://qb.kaufdeinquiz.com/users/dropbox/callback + +# OAuth settings +USE_HTTPS=True # Set to True when behind a reverse proxy with HTTPS offloading +PREFERRED_URL_SCHEME=https # Explicitly set the preferred URL scheme + +# Static OAuth redirect URIs for production environments +# These override the dynamic URLs generated by url_for() when specified +STATIC_OAUTH_URLS=True # Enable static URLs +OAUTH_SPOTIFY_AUTH_URL=https://qb.kaufdeinquiz.com/auth/spotify/callback +OAUTH_SPOTIFY_LINK_URL=https://qb.kaufdeinquiz.com/users/spotify-link/callback +OAUTH_GOOGLE_URL=https://qb.kaufdeinquiz.com/users/login/google/callback +OAUTH_AUTHENTIK_URL=https://qb.kaufdeinquiz.com/users/login/authentik/callback +OAUTH_DROPBOX_URL=https://qb.kaufdeinquiz.com/users/dropbox/callback + +# Important: Make sure these static URLs match your actual server URLs and OAuth provider configurations diff --git a/debug_oauth_urls.py b/debug_oauth_urls.py new file mode 100644 index 0000000..ab1fccf --- /dev/null +++ b/debug_oauth_urls.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +This script helps debug OAuth URL generation when behind a reverse proxy. +It's especially useful for debugging HTTPS redirection issues. + +Usage: + python debug_oauth_urls.py [--https] [--host hostname] [--port portnumber] + +Options: + --https Force HTTPS URL generation regardless of request headers + --host Set the hostname (default: localhost) + --port Set the port number (default: 5000) +""" + +import sys +import os +import argparse +from flask import Flask, request, jsonify +from dotenv import load_dotenv + +# Make sure we can import from the musicround package +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Parse command line arguments +parser = argparse.ArgumentParser(description="Debug OAuth URL generation") +parser.add_argument("--https", action="store_true", + help="Force HTTPS URL generation") +parser.add_argument("--host", default="localhost", + help="Set the hostname (default: localhost)") +parser.add_argument("--port", type=int, default=5000, + help="Set the port number (default: 5000)") +args = parser.parse_args() + +# Load environment variables +load_dotenv() + +# If .env.oauth exists, load it too (it has OAuth specific settings) +if os.path.exists(".env.oauth"): + load_dotenv(".env.oauth") + +# If .env.oauth.production exists and --https is specified, load that instead +if args.https and os.path.exists(".env.oauth.production"): + load_dotenv(".env.oauth.production", override=True) + print("Loading production OAuth settings from .env.oauth.production") + +# Create a small Flask app just for debugging +app = Flask(__name__) + +# Override config with command line args +app.config["USE_HTTPS"] = args.https +app.config["PREFERRED_URL_SCHEME"] = "https" if args.https else "http" + +@app.route("/") +def debug_oauth(): + """Generate debug information for OAuth URLs""" + from musicround.helpers.auth_helpers import get_oauth_redirect_uri + + # Generate endpoints to test + endpoints = [ + ("auth.callback", None), + ("users.spotify_link_callback", None), + ("users.google_callback", None), + ("users.authentik_callback", None), + ("users.dropbox_callback", None) + ] + + # Generate test URLs + test_urls = {} + for endpoint, provider in endpoints: + try: + # We need to be in an app context to use url_for + with app.app_context(): + # Allow KeyErrors to propagate so we know which endpoints don't exist + url = get_oauth_redirect_uri(endpoint, provider) + test_urls[endpoint] = url + except Exception as e: + test_urls[endpoint] = f"ERROR: {str(e)}" + + # Get request info + headers = { + key: value for key, value in request.headers.items() + if key.lower() in ('x-forwarded-for', 'x-forwarded-proto', + 'x-forwarded-host', 'host', 'origin', 'referer') + } + + # Return detailed info + return jsonify({ + "test_urls": test_urls, + "config": { + "USE_HTTPS": app.config.get("USE_HTTPS", False), + "PREFERRED_URL_SCHEME": app.config.get("PREFERRED_URL_SCHEME", "http"), + "STATIC_OAUTH_URLS": app.config.get("STATIC_OAUTH_URLS", False), + "args": { + "https": args.https, + "host": args.host, + "port": args.port + }, + }, + "headers": headers, + "request_info": { + "url": request.url, + "base_url": request.base_url, + "host": request.host, + "scheme": request.scheme, + } + }) + +if __name__ == "__main__": + print(f"Starting OAuth URL Debug server on http://{args.host}:{args.port}") + print(f"USE_HTTPS is set to: {app.config['USE_HTTPS']}") + print(f"PREFERRED_URL_SCHEME is set to: {app.config['PREFERRED_URL_SCHEME']}") + print(f"Visit http://{args.host}:{args.port}/ to see debug information") + + app.run(host=args.host, port=args.port, debug=True) diff --git a/musicround/__init__.py b/musicround/__init__.py index 0523d2a..c427f5c 100644 --- a/musicround/__init__.py +++ b/musicround/__init__.py @@ -100,12 +100,24 @@ def create_app(config=None): # Configure ProxyFix for reverse proxy (e.g., Nginx) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1) + # Create custom HTTPS middleware (will be applied below if USE_HTTPS is True) + class ForceHTTPSMiddleware: + """Middleware to force HTTPS scheme regardless of request headers""" + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + # Force the wsgi.url_scheme to be https + environ['wsgi.url_scheme'] = 'https' + # Also override X-Forwarded-Proto if present + environ['HTTP_X_FORWARDED_PROTO'] = 'https' + return self.app(environ, start_response) + # Create data directory if it doesn't exist data_dir = '/data' if not os.path.exists(data_dir): os.makedirs(data_dir, exist_ok=True) - - # Set the database file path in the data directory + # Set the database file path in the data directory db_path = os.path.join(data_dir, 'song_data.db') # Configure the app @@ -114,6 +126,9 @@ def create_app(config=None): # Set preferred URL scheme for reverse proxy support if app.config.get('USE_HTTPS'): app.config['PREFERRED_URL_SCHEME'] = 'https' + # Apply the HTTPS middleware when USE_HTTPS is True + app.wsgi_app = ForceHTTPSMiddleware(app.wsgi_app) + app.logger.info("Applying ForceHTTPSMiddleware - all URLs will use HTTPS scheme regardless of headers") # Explicitly set the database URI to ensure correct path app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' diff --git a/musicround/config.py b/musicround/config.py index a9942b1..52113e2 100644 --- a/musicround/config.py +++ b/musicround/config.py @@ -69,11 +69,17 @@ class Config: MAIL_RECIPIENT = os.getenv("MAIL_RECIPIENT", "admin@example.com") # Automation settings - AUTOMATION_TOKEN = os.getenv("AUTOMATION_TOKEN", "change-this-token-in-production") - - # Reverse proxy settings + AUTOMATION_TOKEN = os.getenv("AUTOMATION_TOKEN", "change-this-token-in-production") # Reverse proxy settings USE_HTTPS = os.getenv("USE_HTTPS", "False") == "True" # Force HTTPS URL generation - PREFERRED_URL_SCHEME = 'https' if USE_HTTPS else 'http' + PREFERRED_URL_SCHEME = os.getenv("PREFERRED_URL_SCHEME", 'https' if USE_HTTPS else 'http') + + # Static OAuth URL configuration (for production environments) + STATIC_OAUTH_URLS = os.getenv("STATIC_OAUTH_URLS", "False") == "True" + OAUTH_SPOTIFY_AUTH_URL = os.getenv("OAUTH_SPOTIFY_AUTH_URL") + OAUTH_SPOTIFY_LINK_URL = os.getenv("OAUTH_SPOTIFY_LINK_URL") + OAUTH_GOOGLE_URL = os.getenv("OAUTH_GOOGLE_URL") + OAUTH_AUTHENTIK_URL = os.getenv("OAUTH_AUTHENTIK_URL") + OAUTH_DROPBOX_URL = os.getenv("OAUTH_DROPBOX_URL") diff --git a/musicround/helpers/auth_helpers.py b/musicround/helpers/auth_helpers.py index 3c208b8..8937b16 100644 --- a/musicround/helpers/auth_helpers.py +++ b/musicround/helpers/auth_helpers.py @@ -349,22 +349,62 @@ def update_oauth_tokens(user, tokens, auth_provider): def get_oauth_redirect_uri(endpoint, provider=None): """ Generate OAuth redirect URI with proper scheme handling for reverse proxy environments + + This function chooses the redirect URI using the following priority: + 1. Static URL from config (if STATIC_OAUTH_URLS is True) + 2. Dynamic URL generated by url_for() with PREFERRED_URL_SCHEME + 3. Force to HTTPS if USE_HTTPS=True regardless of incoming request """ - # Use Flask's url_for which respects PREFERRED_URL_SCHEME - if provider: - redirect_uri = url_for(endpoint, provider=provider, _external=True) - else: - redirect_uri = url_for(endpoint, _external=True) + # Check if static OAuth URLs are enabled + use_static_urls = current_app.config.get('STATIC_OAUTH_URLS', False) + use_https = current_app.config.get('USE_HTTPS', False) + + # Define mapping from endpoint to config key for static URLs + static_url_mapping = { + 'auth.callback': 'OAUTH_SPOTIFY_AUTH_URL', + 'users.spotify_link_callback': 'OAUTH_SPOTIFY_LINK_URL', + 'users.google_callback': 'OAUTH_GOOGLE_URL', + 'users.authentik_callback': 'OAUTH_AUTHENTIK_URL', + 'users.dropbox_callback': 'OAUTH_DROPBOX_URL' + } + + # First try to use a static URL if enabled and available + redirect_uri = None + if use_static_urls and endpoint in static_url_mapping: + config_key = static_url_mapping[endpoint] + redirect_uri = current_app.config.get(config_key) + if redirect_uri: + current_app.logger.debug(f"Using static OAuth URL for {endpoint}: {redirect_uri}") + else: + current_app.logger.warning( + f"Static OAuth URLs enabled but no URL defined for {endpoint} " + f"(expected config key: {config_key})" + ) + + # If no static URL, use Flask's url_for which respects PREFERRED_URL_SCHEME + if not redirect_uri: + if provider: + redirect_uri = url_for(endpoint, provider=provider, _external=True) + else: + redirect_uri = url_for(endpoint, _external=True) + + # Force HTTPS when USE_HTTPS=True regardless of the generated URL scheme + if use_https and redirect_uri.startswith('http:'): + redirect_uri = 'https:' + redirect_uri[5:] + current_app.logger.info(f"Forcing HTTPS for OAuth redirect URI: {redirect_uri}") # Log details about the generated URL for debugging - use_https = current_app.config.get('USE_HTTPS', False) preferred_scheme = current_app.config.get('PREFERRED_URL_SCHEME', 'http') + static_enabled = "Yes" if use_static_urls else "No" + static_url_used = "Yes" if use_static_urls and redirect_uri and endpoint in static_url_mapping and current_app.config.get(static_url_mapping[endpoint]) else "No" current_app.logger.debug( f"OAuth Redirect URI: {redirect_uri} | " f"Endpoint: {endpoint} | " f"USE_HTTPS: {use_https} | " f"PREFERRED_URL_SCHEME: {preferred_scheme} | " + f"Static URLs enabled: {static_enabled} | " + f"Used static URL: {static_url_used} | " f"Request scheme: {request.scheme if request else 'N/A'} | " f"X-Forwarded-Proto: {request.headers.get('X-Forwarded-Proto', 'N/A') if request else 'N/A'}" ) diff --git a/musicround/routes/oauth_debug.py b/musicround/routes/oauth_debug.py index 83e38bc..b75934c 100644 --- a/musicround/routes/oauth_debug.py +++ b/musicround/routes/oauth_debug.py @@ -18,10 +18,19 @@ def debug_oauth_urls(): Formats: - HTML: Default view with pretty UI - JSON: Add ?format=json or use Accept: application/json header - """ - # Get config settings + """ # Get config settings use_https = current_app.config.get('USE_HTTPS', False) preferred_scheme = current_app.config.get('PREFERRED_URL_SCHEME', 'http') + static_oauth_urls = current_app.config.get('STATIC_OAUTH_URLS', False) + + # Get all static URL configurations + static_urls = { + 'OAUTH_SPOTIFY_AUTH_URL': current_app.config.get('OAUTH_SPOTIFY_AUTH_URL'), + 'OAUTH_SPOTIFY_LINK_URL': current_app.config.get('OAUTH_SPOTIFY_LINK_URL'), + 'OAUTH_GOOGLE_URL': current_app.config.get('OAUTH_GOOGLE_URL'), + 'OAUTH_AUTHENTIK_URL': current_app.config.get('OAUTH_AUTHENTIK_URL'), + 'OAUTH_DROPBOX_URL': current_app.config.get('OAUTH_DROPBOX_URL') + } # Generate all OAuth callback URLs using the helper function oauth_urls = {