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