feat: implement HTTPS middleware and static OAuth URL configuration

This commit is contained in:
Christian Krakau-Louis
2025-06-09 15:32:58 +02:00
parent c6b89781c9
commit 9caf9ec791
6 changed files with 218 additions and 14 deletions
+17 -2
View File
@@ -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}'
+10 -4
View File
@@ -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")
+46 -6
View File
@@ -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'}"
)
+11 -2
View File
@@ -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 = {