Skip to main content

Overview

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

Authentication Flow

Session Management

Session Storage

User sessions store the following data:
session['user_id'] = user.id      # Unique user identifier (8-char UUID)
session['user_name'] = user.name  # Display name
session['user_role'] = user.role  # 'user' or 'admin'

Session Validation

Protected routes check for user_id in the session:
app/utils.py
from flask import session, redirect, url_for, jsonify

# For HTML routes
if 'user_id' not in session:
    return redirect(url_for('auth.login'))

# For API routes
if 'user_id' not in session:
    return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401

Register

Create a new user account with email and password.

Endpoint

POST /register

Request Parameters

fullname
string
required
User’s full name
email
string
required
Valid email address (must be unique)
password
string
required
User password (will be hashed)
confirm-password
string
required
Password confirmation (must match password)

Implementation

app/routes/auth.py
import uuid
from app.models import User, UserSetting, Wallet

@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        fullname = request.form['fullname']
        email = request.form['email']
        password = request.form['password'].strip()
        confirm_password = request.form.get('confirm-password', '').strip()

        # Validate password match
        if password != confirm_password:
            flash('Mật khẩu xác nhận không khớp!', 'error')
            return redirect(url_for('auth.register'))

        # Check if email already exists
        if User.query.filter_by(email=email).first():
            flash('Email này đã được đăng ký!', 'error')
            return redirect(url_for('auth.register'))

        try:
            # Create user with 8-character UUID
            new_user_id = str(uuid.uuid4())[:8]
            new_user = User(id=new_user_id, name=fullname, email=email)
            new_user.set_password(password)
            db.session.add(new_user)
            
            # Create default settings
            db.session.add(UserSetting(user_id=new_user_id))
            
            # Create default cash wallet
            db.session.add(Wallet(
                id=str(uuid.uuid4())[:8],
                user_id=new_user_id,
                name="Ví tiền mặt",
                type="Tiền mặt",
                balance=0
            ))

            db.session.commit()
            flash('Đăng ký thành công! Vui lòng đăng nhập.', 'success')
            return redirect(url_for('auth.login'))
            
        except Exception as e:
            db.session.rollback()
            flash(f'Lỗi hệ thống: {str(e)}', 'error')
            return redirect(url_for('auth.register'))

    return render_template('auth/register.html')

Response

HTTP/1.1 302 Found
Location: /login
Set-Cookie: session=...

Flash message: "Đăng ký thành công! Vui lòng đăng nhập."
Registration automatically creates a default UserSetting record and a cash wallet (“Ví tiền mặt”) with zero balance.

Login

Authenticate with email and password to create a session.

Endpoint

POST /login
POST /

Request Parameters

email
string
required
User’s registered email address
password
string
required
User’s password

Implementation

app/routes/auth.py
@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')

Password Verification

The User model provides secure password checking:
app/models.py
from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    password_hash = db.Column('MatKhau', db.String(200), nullable=False)

    def set_password(self, password):
        """Hash password with PBKDF2-SHA256"""
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        """Verify password against stored hash"""
        return check_password_hash(self.password_hash, password)

Response

HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: session=...; HttpOnly; Path=/
Users are automatically redirected based on their role: admin/admin/users, user/dashboard

Logout

Destroy the current session and redirect to login.

Endpoint

GET /logout

Implementation

app/routes/auth.py
@auth_bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('auth.login'))

Response

HTTP/1.1 302 Found
Location: /login
Set-Cookie: session=; Expires=Thu, 01 Jan 1970 00:00:00 GMT

Password Reset Flow

1. Request Password Reset

Generate a time-limited reset token and send via email.

Endpoint

POST /forgot-password

Request Parameters

email
string
required
Registered email address

Implementation

app/routes/auth.py
import secrets
from datetime import datetime, timedelta
from flask_mail import Message
from app.models import PasswordResetToken

@auth_bp.route('/forgot-password', methods=['GET', 'POST'])
def forgot_password():
    if request.method == 'POST':
        email = request.form['email']
        user = User.query.filter_by(email=email).first()
        
        if not user:
            flash('Email này chưa được đăng ký!', 'error')
            return redirect(url_for('auth.forgot_password'))
            
        # Generate secure token (256 bits)
        token = secrets.token_urlsafe(32)
        expiration = datetime.now() + timedelta(minutes=15)
        
        # Upsert token (update if exists, insert if new)
        reset_entry = PasswordResetToken.query.filter_by(email=email).first()
        if reset_entry:
            reset_entry.token = token
            reset_entry.expires_at = expiration
        else:
            db.session.add(PasswordResetToken(
                email=email, 
                token=token, 
                expires_at=expiration
            ))
            
        db.session.commit()
        
        try:
            # Send email with reset link
            reset_url = url_for('auth.reset_password', token=token, _external=True)
            msg = Message('Khôi phục mật khẩu - AI Finance', recipients=[email])
            msg.body = f"Bấm vào link để đặt lại mật khẩu:\n{reset_url}\nLink hết hạn sau 15 phút."
            mail.send(msg)
            return redirect(url_for('auth.email_sent'))
        except Exception as e:
            print(e)
            flash('Lỗi gửi email.', 'error')

    return render_template('auth/forgot_password.html')

Token Model

app/models.py
class PasswordResetToken(db.Model):
    __tablename__ = 'password_reset_tokens'
    
    email = db.Column('Email', db.String(100), 
                     db.ForeignKey('nguoidung.Email', ondelete='CASCADE'), 
                     primary_key=True)
    token = db.Column('Token', db.String(100), nullable=False)
    expires_at = db.Column('ThoiGianHetHan', db.DateTime, nullable=False)
Reset tokens expire after 15 minutes for security. Each user can only have one active token at a time.

2. Reset Password with Token

Validate token and update user password.

Endpoint

POST /reset-password/<token>

Request Parameters

token
string
required
URL-safe reset token from email
new-password
string
required
New password
confirm-password
string
required
Password confirmation

Implementation

app/routes/auth.py
@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)

Response

HTTP/1.1 302 Found
Location: /login

Flash message: "Thành công! Hãy đăng nhập."

Security Best Practices

Password Hashing

PBKDF2-SHA256

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 passwords
user.password_hash = generate_password_hash('user_password')

# Always use constant-time comparison
if user.check_password('attempted_password'):
    # Login successful

Session Configuration

Configure secure sessions in production:
config.py
class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY')  # Strong random key
    SESSION_COOKIE_SECURE = True      # HTTPS only
    SESSION_COOKIE_HTTPONLY = True    # Prevent XSS
    SESSION_COOKIE_SAMESITE = 'Lax'   # CSRF protection
    PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
Always use a cryptographically secure SECRET_KEY in production. Generate with: python -c 'import secrets; print(secrets.token_hex(32))'

Email Security

For password reset emails:
config.py
class Config:
    MAIL_SERVER = 'smtp.gmail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
For Gmail, use an App Password instead of your account password.

Blueprint Definition

The authentication blueprint is defined as:
app/routes/auth.py
from flask import Blueprint

auth_bp = Blueprint('auth', __name__)
All routes are registered without a URL prefix, making them accessible at the root level:
  • /loginauth.login
  • /registerauth.register
  • /logoutauth.logout
  • /forgot-passwordauth.forgot_password
  • /reset-password/<token>auth.reset_password

Example Integration

Protected Route

app/routes/transaction.py
from flask import Blueprint, session, jsonify
from app.utils import api_login_required

transaction_bp = Blueprint('transaction', __name__)

@transaction_bp.route('/api/transactions', methods=['GET'])
@api_login_required
def get_transactions():
    # Session data is automatically available
    user_id = session['user_id']
    user_name = session['user_name']
    
    transactions = Transaction.query.filter_by(user_id=user_id).all()
    return jsonify([...])

Client-Side Login

// HTML form submission
fetch('/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    email: '[email protected]',
    password: 'securepass123'
  }),
  credentials: 'same-origin'  // Include cookies
})
.then(response => {
  if (response.redirected) {
    window.location.href = response.url;
  }
});

Checking Auth Status

from flask import session

def is_authenticated():
    return 'user_id' in session

def is_admin():
    return session.get('user_role') == 'admin'

def get_current_user():
    if 'user_id' in session:
        return User.query.get(session['user_id'])
    return None

Common Issues

Ensure cookies are enabled and credentials: 'same-origin' is set in fetch requests. Check that SECRET_KEY is properly configured.
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.
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.
Reset tokens expire after 15 minutes. Users must complete the password reset within this window. Request a new reset link if expired.

Next Steps

API Overview

Learn about the complete API architecture

User Model

Explore the User model and relationships

Transaction API

Manage financial transactions

Security Guide

Advanced security configurations

Build docs developers (and LLMs) love