or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authentication.mdauthorization.mdcore-setup.mddatabase.mdindex.mdpassword-management.mdregistration.mdtwo-factor.mdunified-signin.mdutilities.mdwebauthn.md
tile.json

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