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}"&gt;{html.escape(str(message))}</div&gt;'     
    # 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"&gt;                     
Sign in with {provider_name}                 </a&gt;                 '''        
 # Build database login form HTML         db_form = f'''         <form 
method="POST" action="/unified-login{next_param}"&gt;             <input 
type="hidden" name="csrf_token" value="{csrf_token}"/&gt;             <div 
class="form-group"&gt;                 <label for="username" 
class="form-label"&gt;Username</label&gt;                 <input type="text" 
class="form-control form-control-lg" id="username" name="username" required 
autofocus placeholder="Enter your username"&gt;             </div&gt;           
  <div class="form-group"&gt;                 <label for="password" 
class="form-label"&gt;Password</label&gt;                 <input 
type="password" class="form-control form-control-lg" id="passwor
 d" name="password" required placeholder="Enter your password"&gt;             
</div&gt;             <button type="submit" class="btn btn-submit btn-lg 
w-100"&gt;Sign In</button&gt;         </form&gt;         '''         # Build 
complete HTML page         html_content = f'''         <!DOCTYPE html&gt;       
  <html&gt;         <head&gt;             <title&gt;Login - Your App 
Name</title&gt;             <meta name="viewport" content="width=device-width, 
initial-scale=1"&gt;             <link ***@***.***/dist/css/bootstrap.min.css" 
rel="stylesheet"&gt;             <style&gt;                 /* 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&gt;         </head&gt;         <body&gt;             <div 
class="login-container"&gt;                 <div class="login-header"&gt;       
              <h1&gt;Welcome to Your App Name</h1&gt;                     
<p&gt;Sign in to continue</p&gt;                 </div&gt;                 
{messages_html}                 {oauth_buttons}                 <div 
class="divider"&gt;<span&gt;OR</span&gt;</div&gt;                 {db_form}     
        </div&gt;         </body&gt;         </html&gt;         '''         
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: ***@***.***&gt;

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]

Reply via email to