pages/blog/flask-jwt-login.md (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
---
template:
slug: flask-jwt-login
title: Flask-JWT-Extended × Flask-Login
subtitle: Apparently I do webshit now
date: 2020-06-24
---
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](https://flask-login.readthedocs.io); 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](https://jwt.io) 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.
```python
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.
```python
# 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:
```python
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](https://github.com/maxcountryman/flask-login/pull/514)).
|