Kritim Yantra
Aug 15, 2025
"I spent hours building a Flask auth system, only to realize it had security holes big enough to drive a truck through!"
If that sounds familiar, you're not alone. Many developers (myself included) learn the hard way that slapping together a quick /login
endpoint isn’t enough for production. Between shaky error handling, messy file structures, and overlooked security risks, auth systems can quickly become a nightmare.
What if you could build a Flask auth API that’s:
✔ Secure (password hashing, JWT best practices, rate limiting)
✔ Well-structured (clean, scalable folder setup)
✔ Production-ready (proper error handling, logging, and docs)
Let’s build it—step by step—with explanations for every file and decision.
Here’s the structure we’ll use (and why):
/flask_auth_project
│
├── /app
│ ├── __init__.py # Flask app factory
│ ├── extensions.py # DB, JWT, etc. initialization
│ ├── config.py # Config classes (Dev/Prod/Test)
│ │
│ ├── /auth # Auth blueprint
│ │ ├── __init__.py
│ │ ├── models.py # User model
│ │ ├── routes.py # Login, register, logout routes
│ │ ├── schemas.py # Marshmallow validation
│ │ └── utils.py # Password hashing, token helpers
│ │
│ ├── /middleware # Custom middleware
│ │ └── auth_middleware.py # JWT validation
│ │
│ ├── /static/docs # API docs (Swagger/Postman)
│ └── /tests # Tests (pytest)
│
├── requirements.txt # Dependencies
├── .env.example # Env variable template
└── run.py # Entry point
Why this structure?
/tests
. /posts
, /billing
) without chaos.app/extensions.py
– Safely Initialize Toolsfrom flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
db = SQLAlchemy()
jwt = JWTManager()
# Optional: JWT claims for role-based access
@jwt.additional_claims_loader
def add_claims_to_jwt(identity):
return {"is_admin": False} # Customize per user
Key Notes:
flask_jwt_extended
(not flask_jwt
)—it supports refresh tokens and better security.app/auth/models.py
– The User Modelfrom app.extensions import db
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Security Tip:
generate_password_hash
uses PBKDF2 by default). is_verified
, last_login_at
for production.app/auth/schemas.py
– Request Validationfrom marshmallow import Schema, fields, validate
class UserSchema(Schema):
email = fields.Email(required=True)
password = fields.Str(required=True, validate=validate.Length(min=8))
class LoginSchema(UserSchema):
pass # Reuse validation for consistent rules
Why Marshmallow?
"Email is invalid"
).app/auth/routes.py
– Secure Endpointsfrom flask import request, jsonify
from flask_jwt_extended import create_access_token
from app.auth.models import User
from app.auth.schemas import LoginSchema
@auth_bp.route('/register', methods=['POST'])
def register():
data = LoginSchema().load(request.get_json()) # Validates input
if User.query.filter_by(email=data['email']).first():
return {"error": "Email already exists"}, 409
user = User(email=data['email'])
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
return jsonify({"msg": "User created"}), 201
@auth_bp.route('/login', methods=['POST'])
def login():
data = LoginSchema().load(request.get_json())
user = User.query.filter_by(email=data['email']).first()
if not user or not user.check_password(data['password']):
return {"error": "Invalid credentials"}, 401
access_token = create_access_token(identity=user.id)
return jsonify(access_token=access_token)
Critical Details:
401
for auth failures, 409
for conflicts. Add in app/extensions.py
:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(key_func=get_remote_address, default_limits=["200 per day", "50 per hour"])
Then protect login:
@auth_bp.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
...
In config.py
:
class ProdConfig:
JWT_COOKIE_SECURE = True # Only send over HTTPS
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=15)
Log failed logins (add to login()
route):
import logging
logging.warning(f"Failed login attempt for {data['email']}")
Q: Should I use sessions or JWTs?
A: JWTs for APIs (stateless), sessions for server-rendered apps.
Q: How do I handle password resets?
A: Generate a time-limited token, store its hash in the DB, and email a link.
Q: Why not just use Flask-Login?
A: Flask-Login is great for sessions but less flexible for APIs.
Try adding:
user.role
to JWT claims)What’s your #1 auth struggle? Let me know in the comments! 👇
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google