Authentication System¶
This document covers the authentication and authorization system implemented in the School Clubs Management System.
Overview¶
The application uses Flask-Login for session management combined with Werkzeug password hashing for secure authentication.
Key Features:
- Email and password-based authentication
- Secure password hashing (PBKDF2-SHA256)
- Session-based login with "remember me" support
- Role-based access control (students and admins)
- Turbo Flask integration for dynamic UI updates
Architecture¶
Components¶
graph TD
A[Login Request] --> B[auth.login_modal]
B --> C{Valid Credentials?}
C -->|Yes| D[login_user]
D --> E[Redirect to Landing]
C -->|No| F[Turbo Error Update]
F --> G[Display Error]
H[Protected Route] --> I{Authenticated?}
I -->|No| J[@login_required]
J --> K[Redirect to Landing]
I -->|Yes| L[Check Permissions]
L --> M{Authorized?}
M -->|Yes| N[Allow Access]
M -->|No| O[403 Forbidden]
Flask-Login Setup¶
Configuration in __init__.py:
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.login_view = "auth.login_modal"
login_manager.init_app(app)
User Loader:
@login_manager.user_loader
def load_user(user_id):
"""
Load user by ID from the database.
Args:
user_id: String user identifier
Returns:
User object or None if not found
"""
return User.query.get(int(user_id))
Unauthorized Handler:
@login_manager.unauthorized_handler
def unauthorized():
"""
Handle unauthorized access attempts.
Redirects to landing page with warning message.
"""
flash("You must be logged in to access this page.", "warning")
return redirect(url_for("main.landing_page"))
User Model¶
The User model inherits from UserMixin which provides Flask-Login integration:
from flask_login import UserMixin
class User(UserMixin, db.Model):
user_id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(100), nullable=False)
user_type = db.Column(db.String(10), nullable=False) # 'student' or 'admin'
def get_id(self):
"""Required by Flask-Login."""
return str(self.user_id)
UserMixin Properties¶
| Property | Description |
|---|---|
is_authenticated |
Returns True if user has valid credentials |
is_active |
Returns True if account is active |
is_anonymous |
Returns False for authenticated users |
get_id() |
Returns unique user identifier |
Authentication Routes¶
All authentication routes are defined in auth.py using a Blueprint.
Login¶
Route: POST /login_modal
Purpose: Process login form submission via Turbo Flask modal.
@auth.route("/login_modal", methods=["POST"])
def login_modal():
"""
Handle login requests.
Form Data:
email: User's email address
password: Plain text password
Returns:
Redirect to landing page on success
204 No Content with Turbo update on failure
"""
email = request.form.get("email")
password = request.form.get("password")
error = None
user = User.query.filter_by(email=email).first()
if user is None:
error = "No account found with this email"
elif not check_password_hash(user.password_hash, password):
error = "Incorrect password"
else:
login_user(user)
return redirect(url_for("main.landing_page"))
# Show error via Turbo without page reload
if error:
turbo.push(turbo.update(
render_template("error-login.html", error_user=error),
target="login-error-container",
))
return "", 204
Validation:
- Check if user exists by email
- Verify password hash matches
- Login user if valid
- Display error via Turbo if invalid
Turbo Flask Integration
Errors are displayed dynamically without page reloads by updating the login-error-container div.
Logout¶
Route: GET /logout
Purpose: End user session.
@auth.route("/logout")
@login_required
def logout():
"""
Log out the current user.
Returns:
Redirect to landing page
"""
logout_user()
return redirect(url_for("main.landing_page"), 302)
Process:
- Clear session data
- Remove authentication cookies
- Redirect to landing page
Sign Up¶
Route: POST /sign_up
Purpose: Register new user accounts.
@auth.route("/sign_up", methods=["POST"])
def sign_up_post():
"""
Handle new user registration.
Form Data:
first_name: User's first name
last_name: User's last name
email: Email address
password: Plain text password
Returns:
Redirect to landing page on success
204 with Turbo error update on failure
"""
first_name = request.form.get("first_name")
last_name = request.form.get("last_name")
email = request.form.get("email")
password = request.form.get("password")
error = None
# Validation
if not all([first_name, last_name, email, password]):
error = "Please fill in all fields"
elif User.query.filter_by(email=email).first():
error = "Email already registered"
elif len(password) < 8:
error = "Password must be at least 8 characters"
# Create user if valid
if error is None:
hashed_pw = generate_password_hash(password)
new_user = User(
first_name=first_name,
last_name=last_name,
email=email,
password_hash=hashed_pw,
user_type="student",
)
db.session.add(new_user)
db.session.commit()
return redirect(url_for("main.landing_page"))
# Show error via Turbo
if error:
turbo.push(turbo.update(
render_template("error-login.html", error_user=error),
target="signup-error-container",
))
return "", 204
Validation Rules:
- All fields required
- Email must be unique
- Password minimum 8 characters
- New users default to "student" type
Password Security¶
Hashing¶
The application uses Werkzeug's generate_password_hash which implements PBKDF2-SHA256:
from werkzeug.security import generate_password_hash, check_password_hash
# Hashing (registration)
hashed = generate_password_hash(password)
# Result: pbkdf2:sha256:260000$salt$hash
# Verification (login)
is_valid = check_password_hash(stored_hash, password)
Security Features:
- Algorithm: PBKDF2 with SHA-256
- Iterations: 260,000+ (configurable)
- Salt: Unique random salt per password
- Length: 256-bit hash output
Best Practices
- Never store plain text passwords
- Never log passwords
- Use strong hashing algorithms (PBKDF2, bcrypt, argon2)
- Include unique salts per password
Password Requirements¶
Current requirements (implemented in sign_up_post):
- Minimum 8 characters
- No complexity requirements
Enhancement Opportunity
Consider adding:
import re
def validate_password(password):
if len(password) < 8:
return False, "Password must be at least 8 characters"
if not re.search(r"[A-Z]", password):
return False, "Password must contain uppercase letter"
if not re.search(r"[a-z]", password):
return False, "Password must contain lowercase letter"
if not re.search(r"\d", password):
return False, "Password must contain number"
return True, ""
Session Management¶
Configuration¶
Production Warning
Never hardcode secret keys. Use environment variables:
Session Lifecycle¶
- Login: Create session with user ID
- Request: Load user via
@login_manager.user_loader - Logout: Clear session data
Current User¶
Access the authenticated user via current_user:
from flask_login import current_user
if current_user.is_authenticated:
print(f"Logged in as {current_user.email}")
print(f"User type: {current_user.user_type}")
Remember Me¶
Flask-Login supports "remember me" functionality:
# Login with remember me
login_user(user, remember=True) # Cookie lasts for 365 days
# Custom duration
login_user(user, remember=True, duration=timedelta(days=30))
Currently not implemented in the application but can be added to the login form.
Authorization¶
Role-Based Access Control¶
The application implements role-based access with two user types:
| Role | Type Value | Capabilities |
|---|---|---|
| Student | student |
Create/edit own posts, join clubs, apply for membership |
| Administrator | admin |
Full system access, manage all clubs and posts |
Protection Decorators¶
@login_required¶
Ensures user is authenticated:
from flask_login import login_required
@main.route("/browse")
@login_required
def browse():
# Only accessible to authenticated users
return render_template("browse.html")
Custom Role Checks¶
The application implements utility functions for role checking:
Check Admin:
def is_admin(current_user):
"""
Check if user is an administrator.
Args:
current_user: Flask-Login current_user proxy
Returns:
bool: True if user is admin
"""
return current_user.user_type == "admin"
Check Club Member:
def is_member(current_user, club_id):
"""
Check if user is a member, president, or admin.
Args:
current_user: Flask-Login current_user proxy
club_id: Club identifier
Returns:
bool: True if user has member-level access
"""
if current_user.user_type == "admin":
return True
return Membership.query.filter_by(
student_id=current_user.user_id,
club_id=club_id
).first() is not None
Check Club President:
def is_president(current_user, club_id):
"""
Check if user is president or admin.
Args:
current_user: Flask-Login current_user proxy
club_id: Club identifier
Returns:
bool: True if user has president-level access
"""
if current_user.user_type == "admin":
return True
membership = Membership.query.filter_by(
student_id=current_user.user_id,
club_id=club_id
).first()
return membership and membership.is_president
Usage in Routes¶
@main.route("/post/create", methods=["GET", "POST"])
@login_required
def create_post():
club_id = request.form.get("club_id")
# Check if user can post to this club
if not is_member(current_user, club_id):
flash("You must be a member to create posts.", "danger")
return redirect(url_for("main.club_detail", club_id=club_id))
# User is authorized, proceed with post creation
# ...
@main.route("/remove/<int:club_id>/<int:user_id>", methods=["POST"])
@login_required
def remove_member(club_id, user_id):
# Check president/admin access
if not is_president(current_user, club_id):
flash("Only presidents can remove members.", "danger")
return redirect(url_for("main.club_detail", club_id=club_id))
# User is authorized, proceed with member removal
# ...
Admin Panel Access¶
Flask-Admin views use custom access control:
class AdminModelView(ModelView):
"""Base view with admin-only access."""
def is_accessible(self):
"""Check if current user is an admin."""
return current_user.is_authenticated and current_user.user_type == "admin"
def inaccessible_callback(self, name, **kwargs):
"""Handle unauthorized access."""
abort(403)
Security Best Practices¶
Implemented Protections¶
- ✅ Password Hashing
- All passwords hashed with PBKDF2-SHA256
- ✅ Session Security
- Sessions signed with SECRET_KEY
- ✅ SQL Injection Prevention
- SQLAlchemy ORM with parameterized queries
- ✅ Access Control
- Role-based authorization on all protected routes
Recommended Enhancements¶
Production Checklist
- Use environment variables for SECRET_KEY
- Implement rate limiting on login endpoint
- Add CSRF protection with Flask-WTF
- Enable HTTPS in production
- Implement password reset functionality
- Add email verification for new accounts
- Log authentication attempts
- Implement account lockout after failed attempts
- Add two-factor authentication (2FA)
Rate Limiting Example¶
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@auth.route("/login_modal", methods=["POST"])
@limiter.limit("5 per minute")
def login_modal():
# Login logic with rate limiting
pass
CSRF Protection Example¶
from flask_wtf import FlaskForm, CSRFProtect
csrf = CSRFProtect(app)
# In templates
<form method="POST">
{{ csrf_token() }}
<!-- form fields -->
</form>
Testing Authentication¶
Test User Creation¶
from werkzeug.security import generate_password_hash
# Create test user
test_user = User(
first_name="Test",
last_name="User",
email="test@example.com",
password_hash=generate_password_hash("testpass123"),
user_type="student"
)
db.session.add(test_user)
db.session.commit()
Test Login Flow¶
with app.test_client() as client:
# Attempt login
response = client.post("/login_modal", data={
"email": "test@example.com",
"password": "testpass123"
})
# Check if redirected to landing page
assert response.status_code == 302
assert response.location.endswith("/")
# Verify user is logged in
with client.session_transaction() as session:
assert "_user_id" in session
Test Authorization¶
with app.test_client() as client:
# Login as student
client.post("/login_modal", data={
"email": "student@example.com",
"password": "password"
})
# Try to access admin route
response = client.get("/admin/")
assert response.status_code == 403 # Forbidden
Next Steps¶
- Database Models - User model details
- Admin Panel - Admin authentication
- Routes & Views - Protected routes reference