CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-flask-security

Quickly add security features to your Flask application.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

unified-signin.mddocs/

Unified Signin

Unified signin interface supporting multiple authentication methods (password, WebAuthn, magic links, SMS codes) through a single, streamlined user experience.

Capabilities

Unified Signin Forms

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: StringField

Unified Signin Functions

Core 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
    """

Unified Signin Configuration

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
    """

Identity Mapping Utilities

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
    """

Configuration

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']

Usage Examples

Basic Unified Signin Setup

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)

Custom Unified Signin View

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)

Multi-Method Authentication Flow

@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))

Method Setup and Management

@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)

Magic Link Authentication

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'))

SMS Authentication Integration

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'})

Frontend JavaScript Integration

// 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();
    }
});

Signals

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}")

Security Considerations

Token Security

  • Use cryptographically secure random tokens
  • Implement appropriate token expiration times
  • Store tokens securely with proper hashing

Rate Limiting

  • Implement rate limiting for token requests
  • Prevent enumeration attacks on identity endpoints
  • Limit failed verification attempts

Method Validation

  • Validate phone numbers and email addresses before setup
  • Require verification for new authentication methods
  • Implement proper cleanup of expired or unused methods

Identity Privacy

  • Don't reveal whether users exist during signin attempts
  • Use generic error messages for failed authentication
  • Implement consistent response times regardless of user existence

docs

authentication.md

authorization.md

core-setup.md

database.md

index.md

password-management.md

registration.md

two-factor.md

unified-signin.md

utilities.md

webauthn.md

tile.json