How to Add Auth to a Flask App (Flask-Login, Sessions, OAuth)
A practical guide to adding authentication to a Flask app: Flask-Login sessions, Werkzeug password hashing, OAuth, and JWT — with code and honest trade-offs.
Flask is deliberately minimal, which is great until you need something every app needs — login. There’s no built-in User model, no session-management helper, no auth scaffolding. Adding auth to a Flask app means choosing the right small libraries and wiring them together correctly, because the security details are on you.
The good news: the standard Flask auth stack is small, well-understood, and you can have a secure email/password login working in an afternoon. This guide covers that stack with real code, plus OAuth and JWT, and the trade-offs that matter when you’re shipping a SaaS MVP solo.
The Standard Flask Auth Stack
For server-rendered Flask apps, three pieces do the job:
- Werkzeug (ships with Flask) for password hashing.
- Flask-Login for session management — login, logout, “remember me,” and the current-user object.
- SQLAlchemy (or your ORM of choice) to store users.
That’s it. No heavyweight framework, no vendor.
Step 1: Hash Passwords with Werkzeug
Never store plaintext passwords. Werkzeug’s security module is already installed and gives you a salted hash per password. The default is PBKDF2 with SHA-256; for stronger hashing you can pass scrypt or argon2 (argon2 needs an extra dependency).
from werkzeug.security import generate_password_hash, check_password_hash
# On registration:
hashed = generate_password_hash(password) # defaults to pbkdf2:sha256
# For higher security:
# hashed = generate_password_hash(password, method="scrypt")
# On login:
ok = check_password_hash(user.password_hash, password)
Each hash includes a random salt, so two users with the same password get different hashes.
Step 2: Manage Sessions with Flask-Login
Flask-Login handles the session cookie and reloads the user on every request. Set it up once:
from flask import Flask
from flask_login import LoginManager, UserMixin
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"] # signs the session cookie
login_manager = LoginManager(app)
login_manager.login_view = "login"
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String, unique=True, nullable=False)
password_hash = db.Column(db.String, nullable=False)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Under the hood, Flask-Login stores the user’s id in the session and reloads it each request via load_user. The login and logout views:
from flask_login import login_user, logout_user, login_required, current_user
@app.route("/login", methods=["POST"])
def login():
user = User.query.filter_by(email=request.form["email"]).first()
if user and check_password_hash(user.password_hash, request.form["password"]):
login_user(user, remember=True) # creates the session cookie
return redirect(url_for("dashboard"))
flash("Invalid credentials")
return redirect(url_for("login"))
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("login"))
@app.route("/dashboard")
@login_required
def dashboard():
return render_template("dashboard.html", user=current_user)
@login_required guards a route and current_user gives you the logged-in user anywhere. The remember=True flag enables a persistent session that survives browser restarts.
Step 3: Lock Down the Cookies
A login that works isn’t a login that’s safe. In production, set these so the session cookie can’t be read by JavaScript or sent over plain HTTP:
app.config.update(
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_HTTPONLY=True, # no JS access
SESSION_COOKIE_SAMESITE="Lax", # CSRF mitigation
)
For form CSRF protection on POST routes, add Flask-WTF. Don’t skip this — Flask gives you nothing here by default.
Option: OAuth and Social Login
To add “Sign in with Google,” Authlib (or Flask-Dance) handles the OAuth flow. You redirect to the provider, handle the callback, find-or-create the user, then call login_user to create the same Flask-Login session you’d use for password auth.
# Sketch — after the OAuth callback returns a verified profile:
user = User.query.filter_by(email=profile["email"]).first()
if user is None:
user = User(email=profile["email"], password_hash="") # OAuth-only
db.session.add(user)
db.session.commit()
login_user(user)
Validate the OAuth state parameter to prevent CSRF on the callback. If you’re weighing OAuth against passwordless login, the magic link authentication guide covers when email-based auth is simpler.
Sessions vs JWT in Flask
Flask-Login is session-based and that’s the right default for a server-rendered Flask app or a same-origin frontend. Sessions are simple and revocable — log out and the session is gone.
JWT (via Flask-JWT-Extended) makes sense when Flask is a pure API serving a separate frontend or mobile client on another domain. The catch is the same as everywhere: a JWT can’t be revoked before it expires unless you maintain a blocklist, which means a per-request lookup that undercuts the stateless benefit. For most Flask web apps, stick with Flask-Login. Add JWT only when you have a cross-origin client that needs it.
The Part the Stack Doesn’t Cover: Payments
Werkzeug plus Flask-Login gets you a secure logged-in user quickly. It does not get you paid. For a SaaS you still build billing yourself: the Stripe library, a customer record per user, webhook routes for checkout.session.completed and subscription cancellations, plan gating on your views, and a customer portal — then you keep session state and subscription state in sync forever.
Beag removes that work. Instead of bolting a Stripe integration onto your Flask auth, Beag bundles server-validated authentication and Stripe subscriptions together, with no auth backend or webhook routes for you to maintain. After login, the user’s plan and status are available to your frontend, so gating a premium feature is a plan check, not a billing subsystem:
if current_user.plan in ("pro", "enterprise"):
# grant access
...
You keep Flask for your app logic and skip building auth and payments infrastructure from scratch.
What to Use
- Email/password Flask app? Werkzeug + Flask-Login + Flask-WTF, with secure cookies. That’s the whole stack.
- Social login? Add Authlib and reuse the same Flask-Login session.
- Pure API with a separate frontend? Sessions if same-origin, JWT only if cross-origin.
- Need payments live this week? Don’t hand-roll auth-plus-Stripe — use an all-in-one so identity and billing stay in sync.
Add auth before your routes multiply. Every record gets an owner and every view gets a guard — do it first and the rest of the app assumes a known user.
Add auth and Stripe payments to your Flask app fast. Get started with Beag — no webhook routes, no billing subsystem.
For more guides, see how to add auth to a Next.js app or browse the Beag blog.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →