all repos — site @ 5341f6d7f11ebdd7b1e37e9602ce15ccc20d3e03

source for my site, found at icyphox.sh

pages/txt/flask-jwt-login.txt (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:
url: 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)).