Quickly add security features to your Flask application.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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}")