Flask-JWT-Extended × Flask-Login
Apparently I do webshit now
For the past few months, I’ve been working on building a backend for
$STARTUP
, with a bunch of friends. I’ll probably write in detail about
it when we launch our beta. The backend is your bog standard REST API,
built on Flask—if you didn’t guess from the title already.
Our existing codebase heavily relies on Flask-Login; it offers some pretty neat interfaces for dealing with users and their states. However, its default mode of operation—sessions—don’t really fit into a Flask app that’s really just an API. It’s not optimal. Besides, this is what JWTs were built for.
I won’t bother delving deep into JSON web tokens, but the general flow is like so:
- client logs in via say
/login
- a unique token is sent in the response
- each subsequent request authenticated request is sent with the token
The neat thing about tokens is you can store stuff in them—“claims”, as they’re called.
returning an access_token
to the client
The access_token
is sent to the client upon login. The idea is simple,
perform your usual checks (username / password etc.) and login the user
via flask_login.login_user
. Generate an access token using
flask_jwt_extended.create_access_token
, store your user identity in it
(and other claims) and return it to the user in your 200
response.
Here’s the excerpt from our codebase.
access_token = create_access_token(identity=email)
login_user(user, remember=request.json["remember"])
return good("Logged in successfully!", access_token=access_token)
But, for login_user
to work, we need to setup a custom user loader to
pull out the identity from the request and return the user object.
defining a custom user loader in Flask-Login
By default, Flask-Login handles user loading via the user_loader
decorator, which should return a user object. However, since we want to
pull a user object from the incoming request (the token contains it),
we’ll have to write a custom user loader via the request_loader
decorator.
# Checks the 'Authorization' header by default.
app.config["JWT_TOKEN_LOCATION"] = ["json"]
# Defaults to 'identity', but the spec prefers 'sub'.
app.config["JWT_IDENTITY_CLAIM"] = "sub"
@login.request_loader
def load_person_from_request(request):
try:
token = request.json["access_token"]
except Exception:
return None
data = decode_token(token)
# this can be your 'User' class
person = PersonSignup.query.filter_by(email=data["sub"]).first()
if person:
return person
return None
There’s just one mildly annoying thing to deal with, though. Flask-Login insists on setting a session cookie. We will have to disable this behaviour ourselves. And the best part? There’s no documentation for this—well there is, but it’s incomplete and points to deprecated functions.
disabling Flask-Login’s session cookie
To do this, we define a custom session interface, like so:
from flask.sessions import SecureCookieSessionInterface
from flask import g
from flask_login import user_loaded_from_request
@user_loaded_from_request.connect
def user_loaded_from_request(app, user=None):
g.login_via_request = True
class CustomSessionInterface(SecureCookieSessionInterface):
def should_set_cookie(self, *args, **kwargs):
return False
def save_session(self, *args, **kwargs):
if g.get("login_via_request"):
return
return super(CustomSessionInterface, self).save_session(*args, **kwargs)
app.session_interface = CustomSessionInterface()
In essence, this checks the global store g
for login_via_request
and
and doesn’t set a cookie in that case. I’ve submitted a PR upstream for
this to be included in the docs
(#514).
Questions or comments? Send an email.