How to implement both OAuth2 authentication and traditional username/password login simultaneously? #32472
Replies: 5 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': '[email protected]', '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/[email protected]/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.

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