all repos — site @ f66c2c121ae3edd04ec3b8ca9b960cd9f632d8aa

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
   24 June, 2020

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 [1]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 [2]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 ([3]#514).

References

   1. https://flask-login.readthedocs.io/
   2. https://jwt.io/
   3. https://github.com/maxcountryman/flask-login/pull/514