FinAI uses session-based authentication powered by Flask sessions. User passwords are securely hashed using Werkzeug’s generate_password_hash with PBKDF2-SHA256.
All authentication endpoints are defined in the auth blueprint at app/routes/auth.py
Protected routes check for user_id in the session:
app/utils.py
from flask import session, redirect, url_for, jsonify# For HTML routesif 'user_id' not in session: return redirect(url_for('auth.login'))# For API routesif 'user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
@auth_bp.route('/', methods=['GET', 'POST'])@auth_bp.route('/login', methods=['GET', 'POST'])def login(): # If already logged in, redirect if 'user_id' in session: if session.get('user_role') == 'admin': return redirect(url_for('admin.users')) return redirect(url_for('views.dashboard')) if request.method == 'POST': email = request.form['email'] password = request.form['password'] user = User.query.filter_by(email=email).first() if user and user.check_password(password): # Create session session['user_id'] = user.id session['user_name'] = user.name session['user_role'] = user.role # Redirect based on role if user.role == 'admin': return redirect(url_for('admin.users')) return redirect(url_for('views.dashboard')) else: flash('Email hoặc mật khẩu không chính xác.', 'error') return render_template('auth/login.html')
@auth_bp.route('/reset-password/<token>', methods=['GET', 'POST'])def reset_password(token): reset_entry = PasswordResetToken.query.filter_by(token=token).first() # Validate token exists and not expired if not reset_entry or reset_entry.expires_at < datetime.now(): flash('Link không hợp lệ hoặc đã hết hạn!', 'error') return redirect(url_for('auth.forgot_password')) if request.method == 'POST': password = request.form['new-password'] confirm_password = request.form['confirm-password'] if password != confirm_password: flash('Mật khẩu không khớp', 'error') return redirect(url_for('auth.reset_password', token=token)) user = User.query.filter_by(email=reset_entry.email).first() if user: user.set_password(password) db.session.delete(reset_entry) # Remove used token db.session.commit() flash('Thành công! Hãy đăng nhập.', 'success') return redirect(url_for('auth.login')) return render_template('auth/reset_password.html', token=token)
Werkzeug uses PBKDF2-SHA256 with salt for password hashing. This is a secure, industry-standard algorithm resistant to rainbow table and brute-force attacks.
# Never store plaintext passwordsuser.password_hash = generate_password_hash('user_password')# Always use constant-time comparisonif user.check_password('attempted_password'): # Login successful
Ensure cookies are enabled and credentials: 'same-origin' is set in fetch requests. Check that SECRET_KEY is properly configured.
Password reset email not sending
Verify SMTP credentials in .env file. For Gmail, ensure “Less secure app access” is enabled or use an App Password. Check firewall rules for port 587.
401 Unauthorized on API calls
The session cookie must be sent with each request. For AJAX calls, use credentials: 'same-origin'. For CORS requests, ensure proper CORS headers are configured.
Token expired error on password reset
Reset tokens expire after 15 minutes. Users must complete the password reset within this window. Request a new reset link if expired.