Unified signin interface supporting multiple authentication methods (password, WebAuthn, magic links, SMS codes) through a single, streamlined user experience.
Forms providing a unified interface for multiple authentication methods and verification workflows.
class UnifiedSigninForm(Form):
"""
Main unified signin form supporting multiple authentication methods.
Fields:
- identity: User identity field (email, username, or phone)
- passcode: Multi-purpose field for passwords, codes, or magic links
- remember: Remember me checkbox for session persistence
- next: Hidden field for redirect URL after authentication
"""
identity: StringField
passcode: PasswordField
remember: BooleanField
next: HiddenField
class UnifiedVerifyForm(Form):
"""
Unified signin verification form for multi-step authentication.
Fields:
- passcode: Verification code or password field
- remember: Remember me checkbox
"""
passcode: PasswordField
remember: BooleanField
class UnifiedSigninSetupForm(Form):
"""
Unified signin setup form for configuring authentication methods.
Fields:
- identity: Identity field for method setup
- method: Authentication method selection
- phone: Phone number field for SMS setup
"""
identity: StringField
method: SelectField
phone: StringField
class UnifiedSigninSetupValidateForm(Form):
"""
Unified signin setup validation form.
Fields:
- passcode: Validation code for method verification
"""
passcode: StringFieldCore functions for managing unified signin workflows and multi-method authentication.
def us_send_security_token(user, method, **kwargs):
"""
Send security token for unified signin authentication.
Parameters:
- user: User object to send token to
- method: Authentication method ('email', 'sms', 'authenticator')
- kwargs: Additional method-specific parameters
Returns:
True if token sent successfully, False otherwise
"""
def us_verify_token(user, token, method):
"""
Verify unified signin security token.
Parameters:
- user: User object for token verification
- token: Token string to verify
- method: Authentication method used to send token
Returns:
True if token is valid, False otherwise
"""
def us_get_available_methods(user):
"""
Get list of available authentication methods for user.
Parameters:
- user: User object to check available methods for
Returns:
List of available method names ('password', 'webauthn', 'email', 'sms')
"""
def us_setup_method(user, method, **kwargs):
"""
Setup new authentication method for user.
Parameters:
- user: User object to setup method for
- method: Method name to setup ('sms', 'authenticator')
- kwargs: Method-specific setup parameters
Returns:
Setup result object with success status and setup data
"""
def us_validate_setup(user, method, code):
"""
Validate authentication method setup.
Parameters:
- user: User object validating setup
- method: Method being validated
- code: Validation code entered by user
Returns:
True if setup validation successful, False otherwise
"""Configuration functions for managing unified signin settings and method availability.
def configure_unified_signin(app):
"""
Configure unified signin for Flask application.
Parameters:
- app: Flask application instance
"""
def get_unified_signin_config():
"""
Get current unified signin configuration.
Returns:
Dictionary containing unified signin configuration settings
"""
def update_unified_signin_config(**kwargs):
"""
Update unified signin configuration.
Parameters:
- kwargs: Configuration parameters to update
"""Utility functions for mapping different identity types to user accounts.
def uia_phone_mapper(identity):
"""
Phone number mapping utility for unified identity authentication.
Parameters:
- identity: Phone number string to map
Returns:
Normalized phone number for user lookup
"""
def uia_email_mapper(identity):
"""
Email mapping utility for unified identity authentication.
Parameters:
- identity: Email address string to map
Returns:
Normalized email address for user lookup
"""
def uia_username_mapper(identity):
"""
Username mapping utility for unified identity authentication.
Parameters:
- identity: Username string to map
Returns:
Normalized username for user lookup
"""Unified signin is configured through Flask-Security configuration variables:
# Enable unified signin
app.config['SECURITY_UNIFIED_SIGNIN'] = True
# Available authentication methods
app.config['SECURITY_US_ENABLED_METHODS'] = ['password', 'email', 'sms']
# Method setup requirements
app.config['SECURITY_US_SETUP_SALT'] = 'us-setup-salt'
app.config['SECURITY_US_SETUP_WITHIN'] = '1 days'
# Token configuration
app.config['SECURITY_US_TOKEN_VALIDITY'] = 120 # 2 minutes
app.config['SECURITY_US_EMAIL_SUBJECT'] = 'Please sign in'
# SMS configuration
app.config['SECURITY_US_SMS_SERVICE'] = 'Dummy'
app.config['SECURITY_US_SMS_SERVICE_CONFIG'] = {}
# Identity mapping
app.config['SECURITY_IDENTITY_ATTRIBUTES'] = ['email', 'username', 'us_phone_number']from flask import Flask
from flask_security import Security, UserMixin
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SECURITY_UNIFIED_SIGNIN'] = True
app.config['SECURITY_US_ENABLED_METHODS'] = ['password', 'email', 'sms', 'webauthn']
db = SQLAlchemy(app)
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(80), unique=True)
password = db.Column(db.String(255))
us_phone_number = db.Column(db.String(20))
us_totp_secrets = db.Column(db.Text) # JSON field for TOTP secrets
active = db.Column(db.Boolean(), default=True)
security = Security(app, user_datastore)from flask_security import UnifiedSigninForm, us_send_security_token, current_user
@app.route('/signin', methods=['GET', 'POST'])
def unified_signin():
"""Custom unified signin view with method selection."""
form = UnifiedSigninForm()
if form.validate_on_submit():
identity = form.identity.data
passcode = form.passcode.data
# Look up user by identity
user = lookup_identity(identity)
if not user:
flash('User not found')
return render_template('unified_signin.html', form=form)
# Check if passcode is a password
if verify_password(passcode, user.password):
login_user(user, remember=form.remember.data)
return redirect_or('/dashboard')
# Check if it's a magic link token
if us_verify_token(user, passcode, 'email'):
login_user(user, remember=form.remember.data)
return redirect_or('/dashboard')
# Check available methods and offer options
available_methods = us_get_available_methods(user)
if 'email' in available_methods:
us_send_security_token(user, 'email')
flash('Sign-in code sent to your email')
return render_template('unified_signin_verify.html',
user=user, methods=available_methods)
return render_template('unified_signin.html', form=form)@app.route('/signin-verify/<user_id>', methods=['GET', 'POST'])
def signin_verify(user_id):
"""Handle verification step of unified signin."""
user = User.query.get_or_404(user_id)
form = UnifiedVerifyForm()
if form.validate_on_submit():
passcode = form.passcode.data
# Try different verification methods
methods_to_try = ['email', 'sms', 'authenticator']
for method in methods_to_try:
if us_verify_token(user, passcode, method):
login_user(user, remember=form.remember.data)
return redirect_or('/dashboard')
flash('Invalid verification code')
# Show available methods
available_methods = us_get_available_methods(user)
return render_template('signin_verify.html',
form=form,
user=user,
methods=available_methods)
@app.route('/resend-code/<user_id>/<method>')
def resend_signin_code(user_id, method):
"""Resend signin code via specified method."""
user = User.query.get_or_404(user_id)
if us_send_security_token(user, method):
flash(f'New code sent via {method}')
else:
flash(f'Failed to send code via {method}')
return redirect(url_for('signin_verify', user_id=user_id))@app.route('/setup-signin-methods')
@login_required
def setup_signin_methods():
"""Display available signin methods setup."""
available_methods = us_get_available_methods(current_user)
return render_template('setup_signin_methods.html',
methods=available_methods)
@app.route('/setup-method/<method>', methods=['GET', 'POST'])
@login_required
def setup_signin_method(method):
"""Setup specific authentication method."""
form = UnifiedSigninSetupForm()
if method == 'sms':
if form.validate_on_submit():
phone = form.phone.data
result = us_setup_method(current_user, 'sms', phone=phone)
if result.success:
session['us_setup_method'] = method
session['us_setup_phone'] = phone
flash('Verification code sent to your phone')
return redirect(url_for('validate_method_setup'))
else:
flash('Failed to setup SMS authentication')
elif method == 'authenticator':
if request.method == 'POST':
result = us_setup_method(current_user, 'authenticator')
if result.success:
session['us_setup_method'] = method
session['us_totp_secret'] = result.totp_secret
return render_template('setup_authenticator.html',
qr_code=result.qr_code,
secret=result.totp_secret)
return render_template('setup_method.html', form=form, method=method)
@app.route('/validate-method-setup', methods=['GET', 'POST'])
@login_required
def validate_method_setup():
"""Validate authentication method setup."""
form = UnifiedSigninSetupValidateForm()
method = session.get('us_setup_method')
if form.validate_on_submit():
code = form.passcode.data
if us_validate_setup(current_user, method, code):
flash(f'{method.title()} authentication setup successfully')
session.pop('us_setup_method', None)
return redirect(url_for('setup_signin_methods'))
else:
flash('Invalid verification code')
return render_template('validate_setup.html', form=form, method=method)from flask_mail import Message
@app.route('/send-magic-link', methods=['POST'])
def send_magic_link():
"""Send magic link for passwordless authentication."""
identity = request.form.get('identity')
user = lookup_identity(identity)
if user:
# Generate and send magic link token
if us_send_security_token(user, 'email'):
flash('Magic link sent to your email')
else:
flash('Failed to send magic link')
else:
# Don't reveal if user exists
flash('If an account exists, a magic link has been sent')
return redirect(url_for('unified_signin'))
@app.route('/magic-link/<token>')
def magic_link_signin(token):
"""Handle magic link authentication."""
# Verify the token and extract user information
user = None
for u in User.query.all():
if us_verify_token(u, token, 'email'):
user = u
break
if user:
login_user(user)
flash('Successfully signed in via magic link')
return redirect('/dashboard')
else:
flash('Invalid or expired magic link')
return redirect(url_for('unified_signin'))from flask_security import SmsSenderFactory
# Configure SMS service
app.config['SECURITY_SMS_SERVICE'] = 'Twilio'
app.config['SECURITY_SMS_SERVICE_CONFIG'] = {
'ACCOUNT_SID': 'your-twilio-account-sid',
'AUTH_TOKEN': 'your-twilio-auth-token',
'FROM_NUMBER': '+1234567890'
}
@app.route('/setup-sms-signin', methods=['POST'])
@login_required
def setup_sms_signin():
"""Setup SMS-based signin for current user."""
phone = request.form.get('phone')
if phone:
# Validate and normalize phone number
normalized_phone = PhoneUtil.normalize_phone(phone)
# Setup SMS method
result = us_setup_method(current_user, 'sms', phone=normalized_phone)
if result.success:
flash('SMS verification code sent')
return jsonify({'success': True, 'setup_token': result.setup_token})
else:
flash('Failed to setup SMS signin')
return jsonify({'success': False, 'error': 'Setup failed'})
return jsonify({'success': False, 'error': 'Phone number required'})
@app.route('/verify-sms-setup', methods=['POST'])
@login_required
def verify_sms_setup():
"""Verify SMS setup with received code."""
code = request.form.get('code')
if us_validate_setup(current_user, 'sms', code):
# SMS signin is now enabled for user
flash('SMS signin enabled successfully')
return jsonify({'success': True})
else:
flash('Invalid verification code')
return jsonify({'success': False, 'error': 'Invalid code'})// Unified signin with progressive enhancement
class UnifiedSignin {
constructor() {
this.form = document.getElementById('unified-signin-form');
this.identityField = document.getElementById('identity');
this.passcodeField = document.getElementById('passcode');
this.submitButton = document.getElementById('submit-btn');
this.setupEventListeners();
}
setupEventListeners() {
this.form.addEventListener('submit', this.handleSubmit.bind(this));
this.identityField.addEventListener('input', this.handleIdentityInput.bind(this));
}
async handleSubmit(event) {
event.preventDefault();
const identity = this.identityField.value;
const passcode = this.passcodeField.value;
// Try different authentication methods
if (passcode) {
// Submit with passcode (password, code, or token)
this.form.submit();
} else {
// Send magic link or show method selection
await this.requestAuthMethod(identity);
}
}
async handleIdentityInput() {
const identity = this.identityField.value;
if (identity.length > 3) {
// Check available methods for this identity
const methods = await this.getAvailableMethods(identity);
this.updateMethodOptions(methods);
}
}
async getAvailableMethods(identity) {
try {
const response = await fetch('/api/signin-methods', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({identity: identity})
});
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error('Error fetching methods:', error);
}
return {methods: ['password']};
}
updateMethodOptions(methodsData) {
const methods = methodsData.methods || [];
const methodsDiv = document.getElementById('method-options');
if (methods.length > 1) {
methodsDiv.innerHTML = methods.map(method =>
`<button type="button" class="method-btn" data-method="${method}">
Sign in with ${method}
</button>`
).join('');
methodsDiv.style.display = 'block';
// Add click handlers for method buttons
methodsDiv.querySelectorAll('.method-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.selectMethod(btn.dataset.method);
});
});
}
}
async selectMethod(method) {
const identity = this.identityField.value;
if (method === 'email' || method === 'sms') {
// Request token via selected method
try {
const response = await fetch('/api/request-signin-token', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
identity: identity,
method: method
})
});
if (response.ok) {
this.showVerificationStep();
}
} catch (error) {
console.error('Error requesting token:', error);
}
} else if (method === 'webauthn') {
// Initiate WebAuthn authentication
await this.authenticateWithWebAuthn(identity);
}
}
showVerificationStep() {
// Update UI to show code input
this.passcodeField.placeholder = 'Enter verification code';
this.passcodeField.focus();
document.getElementById('method-options').style.display = 'none';
document.getElementById('verification-info').style.display = 'block';
}
async authenticateWithWebAuthn(identity) {
// WebAuthn authentication flow
try {
// Get authentication options
const optionsResponse = await fetch('/webauthn/authenticate/begin', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({identity: identity})
});
const options = await optionsResponse.json();
options.challenge = this.base64ToArrayBuffer(options.challenge);
// Get assertion
const assertion = await navigator.credentials.get({
publicKey: options
});
// Complete authentication
const authResponse = await fetch('/webauthn/authenticate/complete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: assertion.id,
rawId: this.arrayBufferToBase64(assertion.rawId),
response: {
clientDataJSON: this.arrayBufferToBase64(assertion.response.clientDataJSON),
authenticatorData: this.arrayBufferToBase64(assertion.response.authenticatorData),
signature: this.arrayBufferToBase64(assertion.response.signature)
}
})
});
const result = await authResponse.json();
if (result.success) {
window.location.href = result.redirect || '/dashboard';
}
} catch (error) {
console.error('WebAuthn error:', error);
alert('WebAuthn authentication failed');
}
}
base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
}
// Initialize unified signin on page load
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('unified-signin-form')) {
new UnifiedSignin();
}
});Unified signin emits specific signals for tracking authentication events:
from flask_security import us_profile_changed, us_security_token_sent
# Listen to unified signin signals
@us_profile_changed.connect_via(app)
def handle_us_profile_change(sender, user, method, **extra):
"""Handle unified signin profile changes."""
print(f"User {user.email} updated {method} method")
@us_security_token_sent.connect_via(app)
def handle_us_token_sent(sender, user, method, **extra):
"""Handle unified signin token sending."""
print(f"Security token sent to {user.email} via {method}")