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

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_sqlalchemyIniziamo 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'}), 201In questo codice, la funzione register esegue le seguenti operazioni:
- Estrae i dati dal corpo della richiesta.
- Verifica se il nome utente e la password sono forniti.
- Controlla se l'utente esiste già nel database.
- 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}), 200In questa funzione:
- Controlliamo se l'utente esiste e se la password è corretta.
- Se le credenziali sono valide, generiamo un token JWT.
- 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_functionAbbiamo 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}), 200In 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.