How to implement both OAuth2 authentication and traditional username/password login simultaneously? #32472
Replies: 6 comments 10 replies
-
|
Hey @yang472541642! I'm here to help you with any bugs, questions, or contributions you have. Let's tackle this OAuth2 and traditional login implementation together! To implement both OAuth2 authentication and traditional username/password login simultaneously in Apache Superset, you can create a custom security manager and authentication view. Here's an example of how you can achieve this: from flask import redirect, flash, request
from superset.security import SupersetSecurityManager
from flask_login import login_user
from flask_appbuilder.security.views import AuthDBView
from flask_appbuilder.security.views import expose
class CustomAuthDBView(AuthDBView):
@expose('/login/', methods=['GET', 'POST'])
def login(self):
token = request.args.get('token')
if token == 'SOME_TOKEN_VALUE':
user = self.appbuilder.sm.find_user(username=request.args.get('username'))
login_user(user)
return redirect(self.appbuilder.get_url_for_index)
else:
return super(CustomAuthDBView, self).login()
class CustomSecurityManager(SupersetSecurityManager):
authdbview = CustomAuthDBView
def __init__(self, appbuilder):
super(CustomSecurityManager, self).__init__(appbuilder)
CUSTOM_SECURITY_MANAGER = CustomSecurityManagerIn this setup, users can authenticate either via SSO using a token or via the standard username/password method. Adjust the logic to integrate OAuth2 by checking for OAuth2 tokens and handling them appropriately within the To continue talking to Dosu, mention @dosu. Help Dosu learn! Give it feedback: Great Response | Irrelevant Answer | Incorrect Sources | Too Verbose | Hallucination | Bug Report | Other |
Beta Was this translation helpful? Give feedback.
-
|
@dosu I want to achieve that when accessing the Superset homepage, if the user is not logged in, they will be redirected to a page that supports both account password and OAuth2 single sign-on. Then, they can log in by entering their account and password, or click on the corresponding OAuth2 provider icon to log in through OAuth2 authentication. |
Beta Was this translation helpful? Give feedback.
-
|
@dosu this is custom_sso_security_manager.py |
Beta Was this translation helpful? Give feedback.
-
|
any update on this? I am also trying to implement OAuth2 and default username/password login simultaneously, but above solution/suggestion are not working. |
Beta Was this translation helpful? Give feedback.
-
|
Does this help? Implementing Unified OAuth + Database Login in Apache SupersetThis guide documents how to implement a unified login page in Apache Superset that supports both OAuth (e.g., Google) and traditional username/password authentication on the same page. This is particularly useful when you need to support both Google Account-based authentication and legacy users who don't have Google accounts. ProblemBy default, Superset's Flask-AppBuilder framework provides separate login routes for OAuth and database authentication:
However, Superset's React frontend intercepts the Solution OverviewThe solution involves:
Implementation Steps1. Extend SupersetSecurityManagerCreate a custom security manager in your from flask_appbuilder.security.sqla.manager import SecurityManager
from superset.security import SupersetSecurityManager
class CustomSecurityManager(SupersetSecurityManager):
"""
Custom security manager that extends Superset's default security manager.
Key features:
- Support for both OAuth and database authentication
"""
def auth_user_oauth(self, userinfo):
"""
Override OAuth user authentication.
This method is called after OAuth provider returns user information.
It looks up the user in the database and logs them in.
Args:
userinfo: Dictionary containing user information from OAuth provider
(e.g., {'email': 'user@example.com', 'name': 'John Doe'})
Returns:
User object if found and active, None otherwise
"""
import logging
from flask_appbuilder.security.sqla.models import User
log = logging.getLogger(__name__)
email = userinfo.get("email", "").lower().strip()
if not email:
log.warning("OAuth userinfo missing email field")
return None
# Try to find user by email (case-insensitive)
user = (
self.appbuilder.session.query(User)
.filter(
func.lower(User.email) == email,
User.active == True
)
.first()
)
if user:
log.info(f"OAuth login successful for user: {user.username} (email: {email})")
return user
# Fallback: try username if email lookup fails
username = userinfo.get("username", "").lower().strip()
if username:
user = (
self.appbuilder.session.query(User)
.filter(
func.lower(User.username) == username,
User.active == True
)
.first()
)
if user:
log.info(f"OAuth login successful for user: {user.username} (via username fallback)")
return user
log.warning(f"OAuth user not found: email={email}, username={username}")
log.warning("User must be created by admin before OAuth login will work")
return None2. Configure OAuth and AuthenticationIn your import os
from flask_appbuilder.security.manager import AUTH_OAUTH
# OAuth Configuration
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET:
# Enable OAuth authentication
AUTH_TYPE = AUTH_OAUTH
# OAuth providers configuration
OAUTH_PROVIDERS = [
{
"name": "google",
"icon": "fa-google",
"token_key": "access_token",
"remote_app": {
"client_id": GOOGLE_CLIENT_ID,
"client_secret": GOOGLE_CLIENT_SECRET,
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"client_kwargs": {
"scope": "openid email profile",
},
"authorize_url": "https://accounts.google.com/o/oauth2/auth",
"access_token_url": "https://oauth2.googleapis.com/token",
},
}
]
# Map OAuth user info to Superset user fields
OAUTH_USER_INFO = {
"google": {
"email": "email",
"first_name": "given_name",
"last_name": "family_name",
"id": "sub",
"username": "email",
}
}
# Disable auto-user registration (admin must create users)
AUTH_USER_REGISTRATION = False
AUTH_USER_REGISTRATION_ROLE = "Public"
else:
# Fallback to database authentication if OAuth not configured
AUTH_TYPE = AUTH_DB
# Use custom security manager
CUSTOM_SECURITY_MANAGER = CustomSecurityManager
# HTTPS configuration for Cloud Run / reverse proxy
PREFERRED_URL_SCHEME = "https"
ENABLE_PROXY_FIX = True3. Create Unified Login RouteAdd a function to register the unified login route. This should be called after the Flask app is created: def register_unified_login(app):
"""
Register a custom unified login route that shows both OAuth and Database login.
This function creates a Flask route that bypasses Superset's React frontend
and serves a custom HTML page with both authentication options.
Args:
app: Flask application instance
"""
from flask import request, redirect, url_for, flash, Response
from flask_login import login_user
from flask_wtf.csrf import generate_csrf, validate_csrf, CSRFError
import html
import logging
log = logging.getLogger(__name__)
@app.route('/unified-login', methods=['GET', 'POST'])
def unified_login():
"""
Unified login page with both OAuth and Database authentication.
GET: Displays the login page with OAuth buttons and database login form
POST: Processes database login credentials
"""
# Handle POST (database login)
if request.method == 'POST':
try:
# Validate CSRF token
validate_csrf(request.form.get('csrf_token'))
except CSRFError as e:
log.error(f"CSRF Error during login: {e}", exc_info=True)
flash('Security error: Invalid CSRF token. Please try again.', 'error')
return redirect(url_for('unified_login', next=request.args.get('next', '')))
try:
username = request.form.get('username')
password = request.form.get('password')
log.info(f"Login attempt for username: {username}")
if username and password:
# Get security manager from app
from flask import current_app
sm = current_app.appbuilder.sm
user = sm.auth_user_db(username, password)
if user:
log.info(f"Login successful for user: {username} (ID: {user.id})")
login_user(user, remember=False)
next_url = request.args.get('next') or '/superset/welcome/'
# Clean up next_url if it has nested redirects
if '?' in next_url:
next_url = next_url.split('?')[0]
log.info(f"Redirecting to: {next_url}")
return redirect(next_url)
else:
log.warning(f"Login failed: Invalid credentials for username: {username}")
flash('Invalid username or password', 'error')
# Redirect back to show error message
return redirect(url_for('unified_login', next=request.args.get('next', '')))
else:
log.warning("Login attempt with missing username or password")
flash('Please enter both username and password', 'error')
# Redirect back to show error message
return redirect(url_for('unified_login', next=request.args.get('next', '')))
except CSRFError as e:
log.error(f"CSRF Error during login: {e}", exc_info=True)
flash('Security error: Invalid CSRF token. Please try again.', 'error')
# Redirect back to show error message
return redirect(url_for('unified_login', next=request.args.get('next', '')))
except Exception as e:
log.error(f"Login error: {e}", exc_info=True)
flash(f'Login error: {str(e)}', 'error')
# Redirect back to show error message
return redirect(url_for('unified_login', next=request.args.get('next', '')))
# GET - show login page
next_url = request.args.get('next', '')
next_param = f'?next={html.escape(next_url)}' if next_url else ''
# Get OAuth providers
providers = []
try:
from flask import current_app
if hasattr(current_app.appbuilder.sm, 'oauth_remotes'):
providers = list(current_app.appbuilder.sm.oauth_remotes.keys())
except Exception as e:
log.warning(f"Could not get OAuth remotes: {e}")
pass
# Generate CSRF token for the form
csrf_token = generate_csrf()
# Build flashed messages HTML
from flask import get_flashed_messages
messages_html = ""
messages = get_flashed_messages(with_categories=True)
if messages:
for category, message in messages:
alert_class = "alert-error" if category == "error" else f"alert-{category}"
messages_html += f'<div class="alert {alert_class}">{html.escape(str(message))}</div>'
# Build OAuth buttons HTML
oauth_buttons = ""
if providers:
for provider in providers:
provider_name = provider.title()
oauth_buttons += f'''
<a href="/login/{provider}{next_param}" class="btn-oauth">
Sign in with {provider_name}
</a>
'''
# Build database login form HTML
db_form = f'''
<form method="POST" action="/unified-login{next_param}">
<input type="hidden" name="csrf_token" value="{csrf_token}"/>
<div class="form-group">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control form-control-lg" id="username" name="username" required autofocus placeholder="Enter your username">
</div>
<div class="form-group">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control form-control-lg" id="password" name="password" required placeholder="Enter your password">
</div>
<button type="submit" class="btn btn-submit btn-lg w-100">Sign In</button>
</form>
'''
# Build complete HTML page
html_content = f'''
<!DOCTYPE html>
<html>
<head>
<title>Login - Your App Name</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* NICE TO HAVE: Box-sizing reset - can skip if you don't need consistent sizing */
* {{ box-sizing: border-box; }}
/* ESSENTIAL: Basic layout for centering the login form */
body {{
background: #f5f5f5; /* NICE TO HAVE: Specific color - can use any background */
min-height: 100vh;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; /* NICE TO HAVE: Specific fonts - can use default */
}}
/* ESSENTIAL: Container for login form */
.login-container {{
max-width: 420px;
width: 100%;
background: white;
padding: 48px 40px; /* NICE TO HAVE: Specific padding - adjust as needed */
border-radius: 8px; /* NICE TO HAVE: Rounded corners - can remove */
box-shadow: 0 2px 8px rgba(0,0,0,0.1); /* NICE TO HAVE: Shadow effect - can remove */
}}
/* NICE TO HAVE: Header styling - can simplify */
.login-header {{
text-align: center;
margin-bottom: 32px;
}}
.login-header h1 {{
font-size: 28px; /* NICE TO HAVE: Specific size - adjust as needed */
font-weight: 600; /* NICE TO HAVE: Bold weight - can use normal */
color: #1a1a1a; /* NICE TO HAVE: Specific color - can use default */
margin: 0 0 8px 0;
}}
.login-header p {{
color: #666; /* NICE TO HAVE: Specific color - can use default */
font-size: 14px; /* NICE TO HAVE: Specific size - adjust as needed */
margin: 0;
}}
/* ESSENTIAL: OAuth button styling */
.btn-oauth {{
background: white;
border: 1px solid #dadce0;
color: #3c4043;
display: block;
text-align: center;
padding: 12px 16px;
font-size: 14px;
font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */
border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */
text-decoration: none;
transition: all 0.2s; /* NICE TO HAVE: Smooth transitions - can remove */
width: 100%;
margin-bottom: 16px;
}}
/* NICE TO HAVE: Hover effect - can remove if you don't want hover styling */
.btn-oauth:hover {{
background: #f8f9fa;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* NICE TO HAVE: Shadow on hover - can remove */
color: #3c4043;
text-decoration: none;
}}
/* NICE TO HAVE: Fancy "OR" divider with line - can replace with simple text or remove */
.divider {{
margin: 24px 0;
text-align: center;
position: relative;
}}
.divider::before {{
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e0e0e0;
}}
.divider span {{
background: white;
padding: 0 16px;
position: relative;
color: #999; /* NICE TO HAVE: Specific color - can use default */
font-size: 13px; /* NICE TO HAVE: Specific size - adjust as needed */
font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */
}}
/* ESSENTIAL: Form field spacing */
.form-group {{
margin-bottom: 20px; /* Adjust spacing as needed */
}}
/* ESSENTIAL: Label styling */
.form-label {{
font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */
color: #333; /* NICE TO HAVE: Specific color - can use default */
margin-bottom: 8px;
font-size: 14px; /* NICE TO HAVE: Specific size - adjust as needed */
display: block;
text-align: left;
}}
/* ESSENTIAL: Input field styling */
.form-control {{
border: 1px solid #ddd;
border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */
padding: 12px 16px; /* Adjust padding as needed */
font-size: 15px; /* NICE TO HAVE: Specific size - adjust as needed */
transition: border-color 0.2s; /* NICE TO HAVE: Smooth transitions - can remove */
width: 100%;
display: block;
}}
/* NICE TO HAVE: Fancy focus state with shadow - can simplify to just border-color change */
.form-control:focus {{
border-color: #4285f4;
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1); /* NICE TO HAVE: Focus ring - can remove */
outline: none;
}}
/* ESSENTIAL: Submit button styling */
.btn-submit {{
background: #4285f4; /* Use your preferred button color */
border: none;
color: white;
font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */
padding: 12px;
border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */
font-size: 15px; /* NICE TO HAVE: Specific size - adjust as needed */
transition: all 0.2s; /* NICE TO HAVE: Smooth transitions - can remove */
width: 100%;
margin-top: 8px;
}}
/* NICE TO HAVE: Hover effect - can remove if you don't want hover styling */
.btn-submit:hover {{
background: #357ae8; /* Slightly darker on hover */
box-shadow: 0 2px 4px rgba(66, 133, 244, 0.3); /* NICE TO HAVE: Shadow on hover - can remove */
}}
/* ESSENTIAL: Alert box styling (needed for error messages) */
.alert {{
border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */
margin-bottom: 20px;
padding: 12px 16px;
}}
/* ESSENTIAL: Error message styling */
.alert-error {{
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}}
.alert-error {{
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>Welcome to Your App Name</h1>
<p>Sign in to continue</p>
</div>
{messages_html}
{oauth_buttons}
<div class="divider"><span>OR</span></div>
{db_form}
</div>
</body>
</html>
'''
return Response(html_content, mimetype='text/html')
@app.route('/login/', methods=['GET'])
def redirect_to_unified_login():
"""Redirects /login/ to /unified-login to ensure custom page is used."""
next_url = request.args.get('next', '')
return redirect(url_for('unified_login', next=next_url))
print("✓ Unified login route registered at /unified-login")
print("✓ /login/ redirects to /unified-login (shows both OAuth and DB login)")4. Register the RouteIn your # In superset_config.py, after defining register_unified_login function
# This will be called by Superset when the app is initialized
def init_app(app):
"""Initialize the Flask app with custom routes."""
register_unified_login(app)Alternatively, if you're using a custom entrypoint or have access to the app initialization, you can call it directly: # In your entrypoint or app initialization code
from superset.app import create_app
app = create_app()
register_unified_login(app)Key Implementation DetailsCSRF ProtectionFlask-WTF's CSRF protection is essential for the database login form. The implementation:
OAuth Redirect HandlingThe OAuth flow uses Flask-AppBuilder's built-in OAuth handling:
TroubleshootingOAuth redirect_uri_mismatchSymptom: OAuth flow fails with Solution:
Database login returns 405 Method Not AllowedSymptom: POST to Solution:
CSRF token missingSymptom: Database login fails with "The CSRF token is missing." Solution:
OAuth user not foundSymptom: OAuth flow completes but user is redirected back to login. Solution:
|
Beta Was this translation helpful? Give feedback.
-
|
I'm afraid this plan didn't work for me, and I ended up failing to achieve it in the end.
小小鸟
***@***.***
…------------------ 原始邮件 ------------------
发件人: "apache/superset" ***@***.***>;
发送时间: 2025年12月31日(星期三) 上午7:21
***@***.***>;
***@***.******@***.***>;
主题: Re: [apache/superset] How to implement both OAuth2 authentication and traditional username/password login simultaneously? (Discussion #32472)
Does this help?
Implementing Unified OAuth + Database Login in Apache Superset
This guide documents how to implement a unified login page in Apache Superset that supports both OAuth (e.g., Google) and traditional username/password authentication on the same page. This is particularly useful when you need to support both Google Account-based authentication and legacy users who don't have Google accounts.
Problem
By default, Superset's Flask-AppBuilder framework provides separate login routes for OAuth and database authentication:
/login/ - Default login page (redirects to OAuth if configured)
/login/{provider} - OAuth provider login (e.g., /login/google)
/login/db - Database authentication (if AUTH_TYPE = AUTH_OAUTH_DB)
However, Superset's React frontend intercepts the /login/ route, making it difficult to customize the login page to show both options simultaneously. Additionally, when AUTH_TYPE = AUTH_OAUTH is set, the database login option disappears entirely.
Solution Overview
The solution involves:
Creating a custom Flask route that bypasses Superset's React frontend
Rendering a custom HTML page with both OAuth and database login forms
Handling CSRF protection for the database login form
Properly redirecting users after successful authentication
Implementation Steps
1. Extend SupersetSecurityManager
Create a custom security manager in your superset_config.py:
from flask_appbuilder.security.sqla.manager import SecurityManager from superset.security import SupersetSecurityManager class CustomSecurityManager(SupersetSecurityManager): """ Custom security manager that extends Superset's default security manager. Key features: - Support for both OAuth and database authentication """ def auth_user_oauth(self, userinfo): """ Override OAuth user authentication. This method is called after OAuth provider returns user information. It looks up the user in the database and logs them in. Args: userinfo: Dictionary containing user information from OAuth provider (e.g., {'email': ***@***.***', 'name': 'John Doe'}) Returns: User object if found and active, None otherwise """ import logging from flask_appbuilder.security.sqla.models import User log = logging.getLogger(__name__) email = userinfo.get("email", "").lower().strip() if not email: log.warning("OAuth userinfo missing email field") return None # Try to find user by email (case-insensitive) user = ( self.appbuilder.session.query(User) .filter( func.lower(User.email) == email, User.active == True ) .first() ) if user: log.info(f"OAuth login successful for user: {user.username} (email: {email})") return user # Fallback: try username if email lookup fails username = userinfo.get("username", "").lower().strip() if username: user = ( self.appbuilder.session.query(User) .filter( func.lower(User.username) == username, User.active == True ) .first() ) if user: log.info(f"OAuth login successful for user: {user.username} (via username fallback)") return user log.warning(f"OAuth user not found: email={email}, username={username}") log.warning("User must be created by admin before OAuth login will work") return None
2. Configure OAuth and Authentication
In your superset_config.py, configure OAuth and set up the authentication type:
import os from flask_appbuilder.security.manager import AUTH_OAUTH # OAuth Configuration GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET: # Enable OAuth authentication AUTH_TYPE = AUTH_OAUTH # OAuth providers configuration OAUTH_PROVIDERS = [ { "name": "google", "icon": "fa-google", "token_key": "access_token", "remote_app": { "client_id": GOOGLE_CLIENT_ID, "client_secret": GOOGLE_CLIENT_SECRET, "server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration", "client_kwargs": { "scope": "openid email profile", }, "authorize_url": "https://accounts.google.com/o/oauth2/auth", "access_token_url": "https://oauth2.googleapis.com/token", }, } ] # Map OAuth user info to Superset user fields OAUTH_USER_INFO = { "google": { "email": "email", "first_name": "given_name", "last_name": "family_name", "id": "sub", "username": "email", } } # Disable auto-user registration (admin must create users) AUTH_USER_REGISTRATION = False AUTH_USER_REGISTRATION_ROLE = "Public" else: # Fallback to database authentication if OAuth not configured AUTH_TYPE = AUTH_DB # Use custom security manager CUSTOM_SECURITY_MANAGER = CustomSecurityManager # HTTPS configuration for Cloud Run / reverse proxy PREFERRED_URL_SCHEME = "https" ENABLE_PROXY_FIX = True
3. Create Unified Login Route
Add a function to register the unified login route. This should be called after the Flask app is created:
def register_unified_login(app): """ Register a custom unified login route that shows both OAuth and Database login. This function creates a Flask route that bypasses Superset's React frontend and serves a custom HTML page with both authentication options. Args: app: Flask application instance """ from flask import request, redirect, url_for, flash, Response from flask_login import login_user from flask_wtf.csrf import generate_csrf, validate_csrf, CSRFError import html import logging log = logging.getLogger(__name__) @app.route('/unified-login', methods=['GET', 'POST']) def unified_login(): """ Unified login page with both OAuth and Database authentication. GET: Displays the login page with OAuth buttons and database login form POST: Processes database login credentials """ # Handle POST (database login) if request.method == 'POST': try: # Validate CSRF token validate_csrf(request.form.get('csrf_token')) except CSRFError as e: log.error(f"CSRF Error during login: {e}", exc_info=True) flash('Security error: Invalid CSRF token. Please try again.', 'error') return redirect(url_for('unified_login', next=request.args.get('next', ''))) try: username = request.form.get('username') password = request.form.get('password') log.info(f"Login attempt for username: {username}") if username and password: # Get security manager from app from flask import current_app sm = current_app.appbuilder.sm user = sm.auth_user_db(username, password) if user: log.info(f"Login successful for user: {username} (ID: {user.id})") login_user(user, remember=False) next_url = request.args.get('next') or '/superset/welcome/' # Clean up next_url if it has nested redirects if '?' in next_url: next_url = next_url.split('?')[0] log.info(f"Redirecting to: {next_url}") return redirect(next_url) else: log.warning(f"Login failed: Invalid credentials for username: {username}") flash('Invalid username or password', 'error') # Redirect back to show error message return redirect(url_for('unified_login', next=request.args.get('next', ''))) else: log.warning("Login attempt with missing username or password") flash('Please enter both username and password', 'error') # Redirect back to show error message return redirect(url_for('unified_login', next=request.args.get('next', ''))) except CSRFError as e: log.error(f"CSRF Error during login: {e}", exc_info=True) flash('Security error: Invalid CSRF token. Please try again.', 'error') # Redirect back to show error message return redirect(url_for('unified_login', next=request.args.get('next', ''))) except Exception as e: log.error(f"Login error: {e}", exc_info=True) flash(f'Login error: {str(e)}', 'error') # Redirect back to show error message return redirect(url_for('unified_login', next=request.args.get('next', ''))) # GET - show login page next_url = request.args.get('next', '') next_param = f'?next={html.escape(next_url)}' if next_url else '' # Get OAuth providers providers = [] try: from flask import current_app if hasattr(current_app.appbuilder.sm, 'oauth_remotes'): providers = list(current_app.appbuilder.sm.oauth_remotes.keys()) except Exception as e: log.warning(f"Could not get OAuth remotes: {e}") pass # Generate CSRF token for the form csrf_token = generate_csrf() # Build flashed messages HTML from flask import get_flashed_messages messages_html = "" messages = get_flashed_messages(with_categories=True) if messages: for category, message in messages: alert_class = "alert-error" if category == "error" else f"alert-{category}" messages_html += f'<div class="alert {alert_class}">{html.escape(str(message))}</div>' # Build OAuth buttons HTML oauth_buttons = "" if providers: for provider in providers: provider_name = provider.title() oauth_buttons += f''' <a href="/login/{provider}{next_param}" class="btn-oauth"> Sign in with {provider_name} </a> ''' # Build database login form HTML db_form = f''' <form method="POST" action="/unified-login{next_param}"> <input type="hidden" name="csrf_token" value="{csrf_token}"/> <div class="form-group"> <label for="username" class="form-label">Username</label> <input type="text" class="form-control form-control-lg" id="username" name="username" required autofocus placeholder="Enter your username"> </div> <div class="form-group"> <label for="password" class="form-label">Password</label> <input type="password" class="form-control form-control-lg" id="password" name="password" required placeholder="Enter your password"> </div> <button type="submit" class="btn btn-submit btn-lg w-100">Sign In</button> </form> ''' # Build complete HTML page html_content = f''' <!DOCTYPE html> <html> <head> <title>Login - Your App Name</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link ***@***.***/dist/css/bootstrap.min.css" rel="stylesheet"> <style> /* NICE TO HAVE: Box-sizing reset - can skip if you don't need consistent sizing */ * {{ box-sizing: border-box; }} /* ESSENTIAL: Basic layout for centering the login form */ body {{ background: #f5f5f5; /* NICE TO HAVE: Specific color - can use any background */ min-height: 100vh; padding: 20px; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; /* NICE TO HAVE: Specific fonts - can use default */ }} /* ESSENTIAL: Container for login form */ .login-container {{ max-width: 420px; width: 100%; background: white; padding: 48px 40px; /* NICE TO HAVE: Specific padding - adjust as needed */ border-radius: 8px; /* NICE TO HAVE: Rounded corners - can remove */ box-shadow: 0 2px 8px rgba(0,0,0,0.1); /* NICE TO HAVE: Shadow effect - can remove */ }} /* NICE TO HAVE: Header styling - can simplify */ .login-header {{ text-align: center; margin-bottom: 32px; }} .login-header h1 {{ font-size: 28px; /* NICE TO HAVE: Specific size - adjust as needed */ font-weight: 600; /* NICE TO HAVE: Bold weight - can use normal */ color: #1a1a1a; /* NICE TO HAVE: Specific color - can use default */ margin: 0 0 8px 0; }} .login-header p {{ color: #666; /* NICE TO HAVE: Specific color - can use default */ font-size: 14px; /* NICE TO HAVE: Specific size - adjust as needed */ margin: 0; }} /* ESSENTIAL: OAuth button styling */ .btn-oauth {{ background: white; border: 1px solid #dadce0; color: #3c4043; display: block; text-align: center; padding: 12px 16px; font-size: 14px; font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */ border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */ text-decoration: none; transition: all 0.2s; /* NICE TO HAVE: Smooth transitions - can remove */ width: 100%; margin-bottom: 16px; }} /* NICE TO HAVE: Hover effect - can remove if you don't want hover styling */ .btn-oauth:hover {{ background: #f8f9fa; box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* NICE TO HAVE: Shadow on hover - can remove */ color: #3c4043; text-decoration: none; }} /* NICE TO HAVE: Fancy "OR" divider with line - can replace with simple text or remove */ .divider {{ margin: 24px 0; text-align: center; position: relative; }} .divider::before {{ content: ''; position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #e0e0e0; }} .divider span {{ background: white; padding: 0 16px; position: relative; color: #999; /* NICE TO HAVE: Specific color - can use default */ font-size: 13px; /* NICE TO HAVE: Specific size - adjust as needed */ font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */ }} /* ESSENTIAL: Form field spacing */ .form-group {{ margin-bottom: 20px; /* Adjust spacing as needed */ }} /* ESSENTIAL: Label styling */ .form-label {{ font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */ color: #333; /* NICE TO HAVE: Specific color - can use default */ margin-bottom: 8px; font-size: 14px; /* NICE TO HAVE: Specific size - adjust as needed */ display: block; text-align: left; }} /* ESSENTIAL: Input field styling */ .form-control {{ border: 1px solid #ddd; border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */ padding: 12px 16px; /* Adjust padding as needed */ font-size: 15px; /* NICE TO HAVE: Specific size - adjust as needed */ transition: border-color 0.2s; /* NICE TO HAVE: Smooth transitions - can remove */ width: 100%; display: block; }} /* NICE TO HAVE: Fancy focus state with shadow - can simplify to just border-color change */ .form-control:focus {{ border-color: #4285f4; box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1); /* NICE TO HAVE: Focus ring - can remove */ outline: none; }} /* ESSENTIAL: Submit button styling */ .btn-submit {{ background: #4285f4; /* Use your preferred button color */ border: none; color: white; font-weight: 500; /* NICE TO HAVE: Bold weight - can use normal */ padding: 12px; border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */ font-size: 15px; /* NICE TO HAVE: Specific size - adjust as needed */ transition: all 0.2s; /* NICE TO HAVE: Smooth transitions - can remove */ width: 100%; margin-top: 8px; }} /* NICE TO HAVE: Hover effect - can remove if you don't want hover styling */ .btn-submit:hover {{ background: #357ae8; /* Slightly darker on hover */ box-shadow: 0 2px 4px rgba(66, 133, 244, 0.3); /* NICE TO HAVE: Shadow on hover - can remove */ }} /* ESSENTIAL: Alert box styling (needed for error messages) */ .alert {{ border-radius: 4px; /* NICE TO HAVE: Rounded corners - can remove */ margin-bottom: 20px; padding: 12px 16px; }} /* ESSENTIAL: Error message styling */ .alert-error {{ background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }} .alert-error {{ background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }} </style> </head> <body> <div class="login-container"> <div class="login-header"> <h1>Welcome to Your App Name</h1> <p>Sign in to continue</p> </div> {messages_html} {oauth_buttons} <div class="divider"><span>OR</span></div> {db_form} </div> </body> </html> ''' return Response(html_content, mimetype='text/html') @app.route('/login/', methods=['GET']) def redirect_to_unified_login(): """Redirects /login/ to /unified-login to ensure custom page is used.""" next_url = request.args.get('next', '') return redirect(url_for('unified_login', next=next_url)) print("✓ Unified login route registered at /unified-login") print("✓ /login/ redirects to /unified-login (shows both OAuth and DB login)")
4. Register the Route
In your superset_config.py, register the unified login route after the app is created. You can do this by adding it to the init_app method or by using Flask's before_first_request hook:
# In superset_config.py, after defining register_unified_login function # This will be called by Superset when the app is initialized def init_app(app): """Initialize the Flask app with custom routes.""" register_unified_login(app)
Alternatively, if you're using a custom entrypoint or have access to the app initialization, you can call it directly:
# In your entrypoint or app initialization code from superset.app import create_app app = create_app() register_unified_login(app)
Key Implementation Details
CSRF Protection
Flask-WTF's CSRF protection is essential for the database login form. The implementation:
Generates a CSRF token using generate_csrf() on GET requests
Includes the token as a hidden field in the form
Validates the token using validate_csrf() on POST requests
OAuth Redirect Handling
The OAuth flow uses Flask-AppBuilder's built-in OAuth handling:
OAuth buttons link to /login/{provider} (e.g., /login/google)
Flask-AppBuilder handles the OAuth callback at /oauth-authorized/{provider}
The custom auth_user_oauth method is called after successful OAuth authentication
Troubleshooting
OAuth redirect_uri_mismatch
Symptom: OAuth flow fails with redirect_uri_mismatch error.
Solution:
Ensure PREFERRED_URL_SCHEME = "https" is set
Verify the redirect URI in your OAuth provider (e.g., Google Cloud Console) matches exactly: https://your-domain.com/oauth-authorized/google
Database login returns 405 Method Not Allowed
Symptom: POST to /unified-login returns 405.
Solution:
Ensure the route decorator includes methods=['GET', 'POST']
Verify the form's action attribute points to /unified-login
CSRF token missing
Symptom: Database login fails with "The CSRF token is missing."
Solution:
Ensure WTF_CSRF_ENABLED = True in config
Verify the hidden CSRF input field is included in the form
Check that cookies are enabled in the browser
OAuth user not found
Symptom: OAuth flow completes but user is redirected back to login.
Solution:
Verify the user exists in the Superset database with matching email
Check that the user is active (active = True)
Review logs for the exact email/username being looked up
Ensure AUTH_USER_REGISTRATION = False if you want admin-controlled user creation
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
How to implement both OAuth2 authentication and traditional username/password login simultaneously?
Beta Was this translation helpful? Give feedback.
All reactions