Flask-login: authentification par token

Lors de mes derniers dévéloppements j’ai été bloqué par la gestion de l’authentification sur mon back python. J’utilise pour faire les back REST en python une authentification via Flask-login.

Son usage est simple et fonctionne rapidement.

Par défaut l’authentification est géré côté client via l’utilisation de cookie. Depsui Angular 4 les appels REST ne gère plus les cookies.

Il a fallu dnoc imaginer un autre moyen pour maintenir une authentification. J’ai donc modifié mes back pour introduire une authentification par token.

Le principe est le suivant:

  • le client lance la route d’authentification qui lui retourne un token (token qu’on peut trouver dans le cookie)
  • pour le prochain appel, le client ajoutera dans l’entête de l’appel http son token (dans la valeur Authorization dans notre exemple)
  • le back vérifie si il existe un cookie dans l’appel, si non il regarde si l’entete comporte un token pour gérer l’authentification

Pour que le login_manager gère la gestion des cookies il faut implémenter user_loader.

Pour que le login_manager gère la gestion des tokens il faut implémenter request_loader.

Installation

pip install flask
pip install flask-login

Utilisation simple

Un serveur simple avec l’utilisation d’un fichier de configuration

app.cfg

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import logging

DEBUG = False
PATH_STATICS = './statics'
SECRET_KEY = 'secret_key'
PORT = 5000
HOST = '0.0.0.0'
LEVEL = logging.DEBUG
USERS = [
         {  'id'       : 1,
            'username' : 'admin',
            'password' : 'keyadmin',
            'groups'   : ['admin','user']
         },
         {  'id'       : 2,
            'username' : 'test',
            'password' : 'keytest',
            'groups'   : ['user']
         }
        ]

le serveur en lui même

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import logging
import json
from flask import Flask
import flask

from flask_login import LoginManager
from flask_login.utils import encode_cookie, decode_cookie
from flask.ext.login import login_user , logout_user , current_user , login_required

app = Flask(__name__)
app.config.from_pyfile('app.cfg')

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

class User():
    """ class of User, for login"""

    def __init__(self, username):
        self.username = username

    def check_password(self, password):
        if self.username in [i['username']  for i in app.config['USERS']]:
            if [ i['password'] for i in app.config['USERS'] if i['username'] == self.username][0] == password:
                return True
        return False

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return [ i['id'] for i in app.config['USERS'] if i['username']  == self.username][0]

    def __repr__(self):
        return '<User %r>' % (self.username)

@login_manager.user_loader
def user_loader(id):
    return User([i['username']  for i in app.config['USERS'] if i['id']== id][0])

@app.app._login_manager.request_loader
def request_loader(req):
    try:
        id = decode_cookie(req.headers['Authorization'])
        return User([i['username']  for i in app.config['USERS'] if i['id']== id][0])
    except Exception as e:
        return None

## route

@app.route('/test')
@login_required
def index():
    return json.dumps({'value':'login is authorized'})

@app.route('/login',methods=['POST'])
def login():
    username = flask.request.json['username']
    password = flask.request.json['password']
    registered_user = User(username)
    if registered_user.check_password(password):
        login_user(registered_user, remember = True)
        return  json.dumps({'token':encode_cookie(text_type(registered_user.get_id()))})
    return flask.abort(401) # 401 Unauthorized

@app.route('/logout')
def logout():
    logout_user()
    return json.dumps({'value':"logout"})

@login_manager.unauthorized_handler
def unauthorized():
    return json.dumps({'value':"non autoriser bouh !!!"})

if __name__ == "__main__":
    app.run(host = app.config['HOST'],
        port = app.config['PORT'])

Une fois lancé notre serveur

python myserver.py

Au niveau d’angular le service ressemble à

function httpOptions(authorization: string): object {
  return {
    headers: new HttpHeaders({
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': authorization
    })
  }
}

test(token: string): Observable<Result> {
  return this.http.get<Result>(this.url+'/test', httpOptions(token))
    .pipe(
    tap(result => {
        console.log('fetched test');
    }),
    map(user => {
        result = Result.fromJson(user);
        return result;
    }),
    catchError(this.handleError<Result>('Service.test', new Result()))
    );
}

la définition de la classe Result est la suivante

export class Result {
    value: string = "";

    constructor (value: string='') {
        this.value = value;
    }

    public static fromJson(json: Object): Result {
        return new Result(
            json['value']
        );
    }

    public toJson(): Object {
        return {
        'value': this.value
        }
    }
}

Il est possible au niveau du back de décorer les routes par des fonctions qui gère les logins avec des groupes

class Unauthorized(Error):
    def __init__(self, key=""):
        Error.__init__(self,
            status=401,
            title="Unauthorized",
            type="RG LOGIN",
            key="")

    def __str__(self):
        return "Authorization Required"

def check_login(*groups):
    def decorator(func):
        @functools.wraps(func)
        def onCall(*args, **kw):
            if flask_login.current_user.is_anonymous:
                raise Unauthorized()
            if len(groups) and not flask_login.current_user.in_groups(*groups):
                raise Unauthorized()
            if len(groups):
                flask.current_app.logger.debug("%s check groups %s is authorized" % (flask_login.current_user.get_id(), ','.join(*groups)))
            else:
                flask.current_app.logger.debug("%s is authorized" % (flask_login.current_user.get_id()))
            return func(*args, **kw)
        return onCall
    return decorator