python
53 lines · 8 steps
How signed remember-me cookies work in Flask
A Flask login route issues a tamper-proof persistent auth cookie and validates it on later requests.
Explained by
highlit
1from datetime import timedelta
2
3from flask import Blueprint, current_app, make_response, redirect, request, url_for
4from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
5from werkzeug.security import check_password_hash
6
7auth = Blueprint("auth", __name__)
8
9REMEMBER_COOKIE = "remember_token"
10REMEMBER_MAX_AGE = timedelta(days=30)
11
12
13def _serializer():
14 return URLSafeTimedSerializer(current_app.secret_key, salt="remember-me")
15
16
17@auth.post("/login")
18def login():
19 user = User.query.filter_by(email=request.form["email"].lower()).first()
20 if user is None or not check_password_hash(user.password_hash, request.form["password"]):
21 return redirect(url_for("auth.login_form", error="invalid"))
22
23 login_user(user)
24
25 response = make_response(redirect(url_for("dashboard.index")))
26
27 if request.form.get("remember"):
28 token = _serializer().dumps({"uid": user.id, "v": user.token_version})
29 response.set_cookie(
30 REMEMBER_COOKIE,
31 token,
32 max_age=int(REMEMBER_MAX_AGE.total_seconds()),
33 httponly=True,
34 secure=not current_app.debug,
35 samesite="Lax",
36 )
37
38 return response
39
40
41def load_user_from_remember_cookie():
42 token = request.cookies.get(REMEMBER_COOKIE)
43 if not token:
44 return None
45 try:
46 data = _serializer().loads(token, max_age=int(REMEMBER_MAX_AGE.total_seconds()))
47 except (BadSignature, SignatureExpired):
48 return None
49
50 user = User.query.get(data["uid"])
51 if user is None or user.token_version != data["v"]:
52 return None
53 return user
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Signing cookies with a secret key and salt makes them tamper-evident without server-side session storage.
- 2Embedding a token version lets you invalidate all outstanding remember-me cookies by bumping one field.
- 3Setting httponly, secure, and samesite hardens auth cookies against XSS and CSRF theft.
Related explainers
python
from collections.abc import MutableMapping class CaseInsensitiveDict(MutableMapping):
Building a case-insensitive dict in Python
data structures
abstract base classes
dunder methods
Intermediate
8 steps
python
from fastapi import FastAPI, Request, status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse
Redacting sensitive fields in FastAPI errors
validation
error-handling
security
Intermediate
7 steps
java
@Component public class RefreshTokenSuccessHandler implements AuthenticationSuccessHandler { private final RefreshTokenService refreshTokenService;
Issuing JWT and refresh tokens on login in Spring
authentication
jwt
http-cookies
Intermediate
7 steps
ruby
class ApplicationController < ActionController::Base ALLOWED_REDIRECT_HOSTS = [nil, ENV.fetch("APP_HOST", "app.example.com")].freeze def store_return_to(location = request.fullpath)
Safe post-login redirects in Rails
open-redirect
session
authentication
Intermediate
9 steps
python
import click from flask.cli import AppGroup from . import db
Custom user CLI commands in Flask
cli
click
command-group
Intermediate
7 steps
python
import heapq from datetime import datetime from pathlib import Path from typing import Iterator, NamedTuple
Merging sorted log files with heapq
generators
heapq
lazy-evaluation
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-signed-remember-me-cookies-work-in-flask-explained-python-6a5a/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.