Forms, Validation and Authentication
Forms with CSRF protection
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField
from wtforms.validators import DataRequired, Email, Length
class LoginForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email(), Length(max=255)])
password = PasswordField("Password", validators=[DataRequired()])
remember = BooleanField("Remember me")
<form method="post" novalidate>
{{ form.hidden_tag() }} {# includes the CSRF token #}
{{ form.email.label }} {{ form.email(autocomplete="email") }}
{% for e in form.email.errors %}<p class="error">{{ e }}</p>{% endfor %}
{{ form.password.label }} {{ form.password(autocomplete="current-password") }}
<button>Sign in</button>
</form>
Every POST form needs the CSRF token; for fetch calls send it in an X-CSRFToken header.
Password hashing
from werkzeug.security import generate_password_hash, check_password_hash
user.password_hash = generate_password_hash(password) # salted, slow hash
check_password_hash(user.password_hash, attempt)
Never store plain passwords or use fast hashes like MD5/SHA-1 for them.
Flask-Login
from flask_login import UserMixin, login_user, logout_user, login_required, current_user
class User(UserMixin, db.Model): ...
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
@auth_bp.route("/login", methods=["GET", "POST"])
@limiter.limit("10 per minute", methods=["POST"])
def login():
form = LoginForm()
if form.validate_on_submit():
user = db.session.scalar(select(User).filter_by(email=form.email.data.lower()))
if user and check_password_hash(user.password_hash, form.password.data):
login_user(user, remember=form.remember.data)
return redirect(safe_next_url() or url_for("main.index"))
flash("Invalid email or password.", "danger") # same message either way
return render_template("auth/login.html", form=form)
@auth_bp.post("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("main.index"))
Security details:
- Use the same error for unknown email and wrong password, so attackers cannot discover accounts.
- Rate-limit login (Flask-Limiter) and lock accounts after repeated failures.
- Logout is POST, so other sites cannot log users out with an image tag.
- Validate
?next=redirects: only allow relative paths starting with a single/, never//evil.com.
Authorisation
Authentication is "who are you"; authorisation is "what may you do".
from functools import wraps
from flask import abort
def admin_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
abort(404) # do not reveal that the admin area exists
return view(*args, **kwargs)
return wrapped
Check ownership on every object: if project.owner_id != current_user.id: abort(404). Changing an ID in the URL must never expose someone else's data.
Exercise
Implement registration, login, logout and a profile page. Add a rate limit on login, the same error for unknown users and wrong passwords, and an ownership check so users can edit only their own projects. Write tests for each rule.
Check yourself
- What does
form.hidden_tag()add? - Why return the same login error for unknown emails?
- Why is logout a POST route?
Ihr Fortschritt wird nur in diesem Browser gespeichert.
Zur Prüfung einreichen (10 Punkte)