feat: implement HTTPS middleware and static OAuth URL configuration
This commit is contained in:
@@ -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
|
||||||
@@ -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)
|
||||||
+17
-2
@@ -100,12 +100,24 @@ def create_app(config=None):
|
|||||||
# Configure ProxyFix for reverse proxy (e.g., Nginx)
|
# 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)
|
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
|
# Create data directory if it doesn't exist
|
||||||
data_dir = '/data'
|
data_dir = '/data'
|
||||||
if not os.path.exists(data_dir):
|
if not os.path.exists(data_dir):
|
||||||
os.makedirs(data_dir, exist_ok=True)
|
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')
|
db_path = os.path.join(data_dir, 'song_data.db')
|
||||||
|
|
||||||
# Configure the app
|
# Configure the app
|
||||||
@@ -114,6 +126,9 @@ def create_app(config=None):
|
|||||||
# Set preferred URL scheme for reverse proxy support
|
# Set preferred URL scheme for reverse proxy support
|
||||||
if app.config.get('USE_HTTPS'):
|
if app.config.get('USE_HTTPS'):
|
||||||
app.config['PREFERRED_URL_SCHEME'] = '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
|
# Explicitly set the database URI to ensure correct path
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||||
|
|||||||
+10
-4
@@ -69,11 +69,17 @@ class Config:
|
|||||||
MAIL_RECIPIENT = os.getenv("MAIL_RECIPIENT", "admin@example.com")
|
MAIL_RECIPIENT = os.getenv("MAIL_RECIPIENT", "admin@example.com")
|
||||||
|
|
||||||
# Automation settings
|
# Automation settings
|
||||||
AUTOMATION_TOKEN = os.getenv("AUTOMATION_TOKEN", "change-this-token-in-production")
|
AUTOMATION_TOKEN = os.getenv("AUTOMATION_TOKEN", "change-this-token-in-production") # Reverse proxy settings
|
||||||
|
|
||||||
# Reverse proxy settings
|
|
||||||
USE_HTTPS = os.getenv("USE_HTTPS", "False") == "True" # Force HTTPS URL generation
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -349,22 +349,62 @@ def update_oauth_tokens(user, tokens, auth_provider):
|
|||||||
def get_oauth_redirect_uri(endpoint, provider=None):
|
def get_oauth_redirect_uri(endpoint, provider=None):
|
||||||
"""
|
"""
|
||||||
Generate OAuth redirect URI with proper scheme handling for reverse proxy environments
|
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
|
# Check if static OAuth URLs are enabled
|
||||||
if provider:
|
use_static_urls = current_app.config.get('STATIC_OAUTH_URLS', False)
|
||||||
redirect_uri = url_for(endpoint, provider=provider, _external=True)
|
use_https = current_app.config.get('USE_HTTPS', False)
|
||||||
else:
|
|
||||||
redirect_uri = url_for(endpoint, _external=True)
|
# 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
|
# 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')
|
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(
|
current_app.logger.debug(
|
||||||
f"OAuth Redirect URI: {redirect_uri} | "
|
f"OAuth Redirect URI: {redirect_uri} | "
|
||||||
f"Endpoint: {endpoint} | "
|
f"Endpoint: {endpoint} | "
|
||||||
f"USE_HTTPS: {use_https} | "
|
f"USE_HTTPS: {use_https} | "
|
||||||
f"PREFERRED_URL_SCHEME: {preferred_scheme} | "
|
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"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'}"
|
f"X-Forwarded-Proto: {request.headers.get('X-Forwarded-Proto', 'N/A') if request else 'N/A'}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,10 +18,19 @@ def debug_oauth_urls():
|
|||||||
Formats:
|
Formats:
|
||||||
- HTML: Default view with pretty UI
|
- HTML: Default view with pretty UI
|
||||||
- JSON: Add ?format=json or use Accept: application/json header
|
- 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)
|
use_https = current_app.config.get('USE_HTTPS', False)
|
||||||
preferred_scheme = current_app.config.get('PREFERRED_URL_SCHEME', 'http')
|
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
|
# Generate all OAuth callback URLs using the helper function
|
||||||
oauth_urls = {
|
oauth_urls = {
|
||||||
|
|||||||
Reference in New Issue
Block a user