Python

Come implementare l'autenticazione con JWT in un'applicazione Flask

Come implementare l'autenticazione con JWT in un'applicazione Flask
LT
Luca Terribili
Autore

Il primo equivoco su JWT è pensare che sia cifrato: non lo è. Un JSON Web Token è firmato, non cifrato — chiunque intercetti il token può decodificare il payload in chiaro con un semplice base64, e leggerlo è banale quanto aprire un file di testo. La firma garantisce che nessuno l'abbia modificato, non che nessuno lo possa leggere. Mettere una password o un dato sensibile dentro il payload di un JWT è un errore che si vede più spesso di quanto si vorrebbe.

La struttura è sempre la stessa: header, payload e firma, concatenati con dei punti. L'header dice quale algoritmo di firma è stato usato, il payload contiene i claim — tipicamente l'ID utente e una scadenza (exp) — e la firma finale garantisce l'integrità di tutto il resto usando una chiave segreta lato server.

Il vantaggio reale su un'architettura a sessioni classiche è che il server non deve tenere in memoria o su database nessuno stato: il token stesso porta tutte le informazioni necessarie, e questo semplifica parecchio la scalabilità quando hai più istanze del server dietro un load balancer, senza dover condividere le sessioni tra di esse. Questo approccio non solo semplifica la gestione delle sessioni, ma consente anche di costruire applicazioni scalabili e sicure, in cui non è necessario mantenere lo stato lato server. In questo articolo, esploreremo come implementare un sistema di autenticazione basato su JWT in un'applicazione web realizzata con Flask.

Un JSON Web Token (JWT) è un metodo di rappresentazione sicura e compatta delle informazioni, utile per l'autenticazione tra il client e il server. La struttura di un JWT è composta da tre sezioni: l'intestazione (header), il payload e la firma. L'intestazione contiene informazioni sul tipo di token e sull'algoritmo di firma utilizzato. Il payload contiene i dati effettivi che vogliamo trasmettere, come l'ID dell'utente. La firma serve per garantire che il token non venga alterato durante il suo percorso. La grande vantaggio di JWT risiede nel fatto che sono auto-contenuti, il che significa che tutte le informazioni necessarie per l'autenticazione sono incluse nel token stesso, evitando la necessità di memorizzare sessioni sul server.

Creazione dell'applicazione Flask

Per cominciare, installiamo Flask e altre librerie necessarie per lavorare con JWT, come pyjwt e flask_sqlalchemy. La libreria flask_sqlalchemy ci permette di gestire facilmente il database, mentre pyjwt sarà utilizzata per generare e validare i JWT.

Per installare le librerie, esegui il comando:

pip install Flask pyjwt flask_sqlalchemy

Iniziamo a configurare la nostra applicazione Flask. Creiamo un file app.py e definiremo una semplice applicazione Flask con un modello di database per gli utenti. Ecco come possiamo fare:

from flask import Flask, request, jsonify
import jwt
import datetime
from functools import wraps
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'  # Cambia con una chiave segreta
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# Modello User per la nostra app
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    password = db.Column(db.String(50), nullable=False)

    def __repr__(self):
        return f"User('{self.username}')"

# Crea il database
with app.app_context():
    db.create_all()

if __name__ == "__main__":
    app.run(debug=True)

Abbiamo configurato un'applicazione di base in Flask con un database SQLite. Il modello User è stato definito per memorizzare gli utenti con un campo username e password. La chiave segreta SECRET_KEY è utilizzata per firmare il token JWT, ed è importante cambiarla con una chiave unica e sicura per la tua applicazione.

Registrazione di un nuovo utente

La registrazione di un nuovo utente è il primo passo verso l'autenticazione. Creiamo una route /register che accetta una richiesta POST per registrare un utente. Il nome utente e la password vengono presi dal corpo della richiesta, quindi verifichiamo se l'utente esiste già nel database. Se l'utente non esiste, lo creiamo e memorizziamo nel database. Ecco come:

@app.route('/register', methods=['POST'])
def register():
    data = request.get_json()

    username = data.get('username')
    password = data.get('password')

    if not username or not password:
        return jsonify({'message': 'Username and password are required'}), 400

    # Verifica se l'utente esiste già
    if User.query.filter_by(username=username).first():
        return jsonify({'message': 'User already exists'}), 400

    # Crea un nuovo utente
    new_user = User(username=username, password=password)
    db.session.add(new_user)
    db.session.commit()

    return jsonify({'message': 'User created successfully'}), 201

In questo codice, la funzione register esegue le seguenti operazioni:

  1. Estrae i dati dal corpo della richiesta.
  2. Verifica se il nome utente e la password sono forniti.
  3. Controlla se l'utente esiste già nel database.
  4. Se l'utente non esiste, viene creato e salvato nel database.

Autenticazione con JWT

Ora che abbiamo un sistema di registrazione, possiamo implementare il login degli utenti. La route /login restituirà un JWT quando le credenziali dell'utente sono corrette. Questo è fondamentale perché il JWT rappresenta l'autenticazione dell'utente e sarà utilizzato per accedere a endpoint protetti. Quando l'utente inserisce il nome utente e la password, Flask verifica se queste corrispondono ai dati nel database. Se sono corretti, viene generato un JWT che contiene l'ID dell'utente e la scadenza del token. Ecco come implementare il login:

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()

    username = data.get('username')
    password = data.get('password')

    if not username or not password:
        return jsonify({'message': 'Username and password are required'}), 400

    user = User.query.filter_by(username=username).first()

    # Verifica se l'utente esiste e la password è corretta
    if not user or user.password != password:
        return jsonify({'message': 'Invalid credentials'}), 401

    # Creazione del JWT
    token = jwt.encode({
        'user_id': user.id,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }, app.config['SECRET_KEY'], algorithm='HS256')

    return jsonify({'token': token}), 200

In questa funzione:

  1. Controlliamo se l'utente esiste e se la password è corretta.
  2. Se le credenziali sono valide, generiamo un token JWT.
  3. Il token contiene l'ID dell'utente e una scadenza di un'ora. La scadenza è importante per limitare la durata di validità del token.

Proteggere gli endpoint con JWT

Una delle caratteristiche principali di JWT è che possiamo usarlo per proteggere gli endpoint della nostra applicazione. Creiamo un decoratore token_required, che verificherà che ogni richiesta a un endpoint protetto contenga un token valido. Se il token non è presente o è invalido, verrà restituito un errore. Ecco come:

def token_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = request.headers.get('Authorization')

        if not token:
            return jsonify({'message': 'Token is missing!'}), 403

        try:
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            current_user = User.query.get(data['user_id'])
        except Exception as e:
            return jsonify({'message': 'Token is invalid!'}), 403

        return f(current_user, *args, **kwargs)

    return decorated_function

Abbiamo definito il decoratore token_required che estrae il token dall'intestazione Authorization della richiesta, lo decodifica e verifica la sua validità. Se il token è valido, prosegue con l'esecuzione della funzione, passando l'utente corrente come parametro. Se il token è mancante o invalido, viene restituito un errore.

Ora, possiamo applicare questo decoratore a qualsiasi endpoint che desideriamo proteggere, come ad esempio una route che restituisce i dettagli dell'utente:

@app.route('/profile', methods=['GET'])
@token_required
def profile(current_user):
    return jsonify({'username': current_user.username}), 200

In questa route, l'utente dovrà fornire un token valido per accedere alle informazioni del proprio profilo.

Conclusione

Il compromesso è la revoca: con le sessioni classiche puoi invalidare un accesso lato server in qualsiasi momento, con JWT stateless no — il token resta valido finché non scade, anche se nel frattempo l'utente ha cambiato password o è stato bannato. La soluzione comune è usare token di breve durata (pochi minuti) accompagnati da un refresh token più longevo per rinnovarli, invece che un unico token valido per giorni.

Domande Frequenti
Che cos'è un JSON Web Token (JWT)?
Un JSON Web Token (JWT) è un metodo di rappresentazione sicura e compatta delle informazioni, utile per l'autenticazione tra il client e il server. La struttura di un JWT è composta da tre sezioni: l'intestazione (header), il payload e la firma.
A cosa serve l'autenticazione in un'applicazione web?
L'autenticazione è un componente cruciale per qualsiasi applicazione web moderna. Serve a gestire l'accesso degli utenti e garantire che le informazioni siano fruibili solo da coloro che hanno il permesso di vederle.
Quali sono i vantaggi dell'utilizzo di JSON Web Tokens?
I vantaggi dell'utilizzo di JSON Web Tokens includono la semplificazione della gestione delle sessioni, la costruzione di applicazioni scalabili e sicure, e la possibilità di evitare la necessità di mantenere lo stato lato server.
Cosa rappresenta la sezione 'payload' di un JSON Web Token?
La sezione 'payload' di un JSON Web Token contiene i dati effettivi che vogliamo trasmettere, come l'ID dell'utente.
A che serve la firma in un JSON Web Token?
La firma in un JSON Web Token serve per garantire che il token non venga alterato durante il suo percorso.
Che cos'è Flask?
Flask è un microframework web scritto in Python. Viene utilizzato per creare facilmente applicazioni web e API RESTful.
A cosa servono le librerie pyjwt e flask_sqlalchemy?
Le librerie pyjwt e flask_sqlalchemy sono utilizzate per lavorare con JSON Web Tokens e per gestire facilmente il database in un'applicazione Flask, rispettivamente.
Vedi tutti →