Gestire l'autenticazione in Flask con flask-jwt-extended

Pubblicato il - lettura in 10 min
Ludus Russo

Ludus Russo

Un maker, un robotico, un Roker!

Recentemente mi sto spostando sempre di più verso lo sviluppo di applicazioni Web single-page. A differenza del metodo classico che ho molto esplorato in questo blog, in questo tipo di architettura l’intera applicazione web viene scaricata la prima volta, quando si accede all’url principale del sito internet. In questo modo, il nostro server principale può scambiare solo i dati da visualizzare all’interno dell’applicazione, invece che dover mandare l’intera pagina da renderizzare ogni volta.

Questo, come è possibile immaginare, semplifica e alleggerisce notevolmente il lavoro del server, rendendo di fatto le applicazioni più scalabili e più semplici da gestire. Inoltre, gestendo lo scambio dati con API basate su standard come JSON, è anche possibile poi sviluppare applicazioni Desktop/Mobile native che comunicano con le stesse API della webapp.

Uno dei problemi principali da gestire, con questa nuova architettura, è la protezione dei dati (e quindi l’autenticazione dell’utente). Vediamo in seguito come fare sfruttando una tecnologia chiamata JSON Web Token (JWT).

JWT: JSON Web Token

JWT è uno standard Open per creare chiavi (token) di accesso tra un server e un client.

Il token JWT viene creato (su richiesta) dal server (solitamente dopo l’autenticazione), e consegnato al client. Ad ogni richiesta che necessita di autenticazione, il client deve inviare anche il token, tramite il quale il server stesso verificherà che il client sia correttamente autenticato.

Il token JWT è diviso in tre parti: l’header, il payload e la firma.

  • L’header contiene informazioni tecniche sul tipo di tecnologia utilizzata per codificare il token;
  • Il payload contiene le informazioni che vogliamo siano contenute nel token stesso, ad esempio l’id dell’utente loggato nel server;
  • La firma serve per controllare che il token non sia stato modificato, e contiene le informazioni precedenti ma codificate usando una chiave segreta nota solo al server.

Queste tre informazioni vengono codificate in base64 e quindi il token viene generato concatenandole in ordine separate da punti:

token = encode(header) + "." + encode(payload) + "." + encode(firma)

Uso di flask-jwt-extended

flask-jwt-extended è un’estensione di flask che fornire tutto il necessario per gestire l’autenticazione usando JWT. Vediamo con un piccolo esempio come usarla e come funziona.

Setup Ambiente di sviluppo

Per prima cosa, creiamo un ambiente virtuale python e installiamo flask e flask-jwt-extended:

$ virtualenv jwt-server && cd jwt-server
$ source bin/activate
(jwt-server)$ pip install flask flask-jwt-extended

Sviluppo di una semplice applicazione

Sviluppiamo adesso una semplicissima applicazione, che permette di richiedere il token di accesso attraverso delle API di Login (in JSON) e quindi di accedere a delle API protette grazie al Token ottenuto. Sempre la stessa App, permette accesso ad API pubbliche senza necessariamente avere a disposizione un token di accesso.

Import e inizializzazione dell’app

Per prima cosa, importiamo i moduli (flask e flask_jwt_extended), inizializziamo l’app, settando anche la secret_key (che sarà la chiave grazie alla quale cripteremo i token), ed inizializziamo il modulo jwt.

from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity

app = Flask(__name__)
app.secret_key = 'super-secret'

jwt = JWTManager(app)

Creazione del database utenti

È necessario creare un database di utenti. Essendo questa un’app di test, per il momento creerò un semplice dizionario, in cui all’interno vengono salvati username e password degli utenti nella forma

{
	'username1': 'password1',
	'username2': 'password2',
	...
	'usernameN': 'passwordN',
}

Nel mio dizionario, creerò due utenti principali (ovviamente può essere esteso a piacimento).

users = {
        'ludovico': 'ludo',
        'user': 'password'
    }

Creazione di API non protette

Per creare un’API non protetta, possiamo banalmente evitare di usare flask_jwt_extended e procedere normalmente:

@app.route('/unprotected', methods=['GET'])
def unprotected():
    return jsonify({'hello': 'unprotected'}), 200

Questo codice, genera un nuovo endpoint nella nostra applicazione sull’url /unprotected, accessibile da metodo GET. L’API ritorna semplicemente il JSON {'hello': 'unprotected'}.

Creazione di API protette

Per creare un’API protetta, flask_jwt_extended ci fornisce il decorator @jwt_required, che permette di accedere all’endpoint solo dopo la verifica del Token. Inoltre, grazie al metodo get_jwt_identity possiamo accedere ai dati salvati nel token, che in questo caso è lo username dell’utente (vedremo dopo come inserire questo dato nel token):

@app.route('/protected', methods=['GET'])
@jwt_required
def protected():
    current_username = get_jwt_identity()
    return jsonify({'hello_from': current_username}), 200

Come vedete, in questo caso creiamo un nuovo endpoint sull’url /protected accessibile da method GET, lo proteggiamo tramite @jwt_required. All’interno dell’applicazione, accediamo allo username dell’utente chiamando la funzione get_jwt_identity() e ritorniamo un json nella forma {'hello_from': username}, dove username dipende dall’utente chiamante.

Generazione del Token e Login

Manca quindi solo l’implementazione di un endpoint per fare il login. L’endpoint /login riceverà nella richiesta lo username e la password dell’utente che si vuole loggare, una volta verificato che queste siano corrette, ritornerà il token JWT contenente nel payload lo username dell’utente stesso.

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username', None)
    password = request.json.get('password', None)
    if username in users and users[username] == password:
        ret = {'access_token': create_access_token(identity=username)}
        return jsonify(ret), 200
    return jsonify({"msg": "Bad username or password"}), 401

In questo caso, abbiamo creato un endpoint (pubblico) sull’url /login che risponde al metodo GET. Per prima cosa, accediamo a username e password contenute nella richiesta, con le righe

    username = request.json.get('username', None)
    password = request.json.get('password', None)

Controlliamo quindi che lo username esiste e che questo metcha correttamente con la password:

    if username in users and users[username] == password:

In caso positivo, generiamo un nuovo token con la funzione create_access_token(identity=username), a cui passiamo come parametro identity lo username dell’utente, e ritorniamo un json contenente tale token.

Altrimenti, ritorniamo (con errore 401) un json che informa l’app che lo username e la password non sono corretti.

Codice completo

Il codice completo è il seguente

from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity

app = Flask(__name__)
app.secret_key = 'super-secret'

jwt = JWTManager(app)

users = {
        'ludovico': 'ludo',
        'user': 'password'
    }

@app.route('/unprotected', methods=['GET'])
def unprotected():
    return jsonify({'hello': 'unprotected'}), 200

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username', None)
    password = request.json.get('password', None)
    if username in users and users[username] == password:
        ret = {'access_token': create_access_token(identity=username)}
        return jsonify(ret), 200
    return jsonify({"msg": "Bad username or password"}), 401

@app.route('/protected', methods=['GET'])
@jwt_required
def protected():
    current_username = get_jwt_identity()
    return jsonify({'hello_from': current_username}), 200

if __name__ == '__main__':
    app.run()

Test dell’applicazione

Per testare l’applicazione, conviene usare un tool per la generazione di richieste verso un server. Personalmente mi trovo molto comodo con il tool Insomnia, che permette di testare API restful in modo molto intuitivo.

Per prima cosa, vediamo se l’applicazione risponde correttamente all’url /unprotected. Se facciamo una richiesta GET su questo URL, infatti, dovremmo ottenere come risposta

{
	"hello": "unprotected"
}

Se proviamo, similmente, a mandare una richiesta sul metodo /protected, invece, dovremmo ottenere la risposta di errore di autenticazione:

{
	"msg": "Missing Authorization Header"
}

Per accedere all’url /protected, dobbiamo prima di tutto ottenere un token, per farlo, mandiamo una richiesta POST all’url /login con un json contenente username e password:

{
	"username": "ludovico",
	"password": "ludo"
}

Se il login non è corretto, otterremo come risultato

{
	"msg": "Bad username or password"
}

altrimenti avremmo finalmente il nostro token:

{
	"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2NsYWltcyI6e30sImp0aSI6ImMxOGE3MWMxLTM4Y2EtNGYwNi1hNzYwLWY0YTVlOGUxMGZhMiIsImV4cCI6MTQ5NzI2MjQ3NiwiZnJlc2giOmZhbHNlLCJpYXQiOjE0OTcyNjE1NzYsInR5cGUiOiJhY2Nlc3MiLCJuYmYiOjE0OTcyNjE1NzYsImlkZW50aXR5IjoibHVkb3ZpY28ifQ.Eru4JFykJzqkNx7epmUkxRW82JfYUN5b2OdrG_osGe4"
}

A questo punto, possiamo finalmente fare una richiesta sull’url /protected. Ricordatevi di risettare il metodo a GET, prima di fare la richiesta, accediamo al tab Auth e settiamo come metodo di autenticazione Bearer Token, settiamo il token ottenuto e quindi facciamo la richiesta. Dovremmo ottenere come risultato:

{
	"hello_from": "ludovico"
}

Conclusioni

Al momento sto investigando l’utilizzo di JWT con alcune librerie client web come ad esempio Angular. Seguiranno a breve altri tutorial sull’argomento.

Ludus Russo

Ludus Russo

Un maker, un robotico, un Roker!

comments powered by Disqus
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora