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
- 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.
access_token to the client
access_token is sent to the client upon login. The idea is simple,
perform your usual checks (username / password etc.) and login the user
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
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)
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
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
# 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
and doesn’t set a cookie in that case. I’ve submitted a PR upstream for
this to be included in the docs