GitHub user yang472541642 added a comment to the discussion: How to implement
both OAuth2 authentication and traditional username/password login
simultaneously?
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.getLo
gger(__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 succ
essful 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 a
nd 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 p
rovider 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="passwor
d" 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 te
xt 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: Round
ed 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: ***@***.***>
GitHub link:
https://github.com/apache/superset/discussions/32472#discussioncomment-15401218
----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]