Guida completa alla generazione e alla verifica di JWT (JSON Web Token) con Python

In questa guida, esploreremo come utilizzare i JWT (JSON Web Token) in Python per migliorare l’autenticazione e la sicurezza, dalla generazione alla verifica. Il nostro obiettivo è quello di fornirti una comprensione completa del processo di autenticazione basato su JWT utilizzando le librerie Python, dalle basi fino agli esempi di implementazione e alle misure di sicurezza. Approfondiamo le conoscenze necessarie per costruire una funzionalità di autenticazione affidabile per le applicazioni web e lo sviluppo delle API.

Indice

Concetti di base e funzionamento di JWT


JWT (JSON Web Token) è uno dei formati di token più utilizzati per l’autenticazione web. JWT si basa su uno standard di trasmissione sicura delle informazioni tra client e server (RFC 7519) ed è dotato di una funzionalità di protezione contro le manomissioni tramite firma.

Struttura di JWT


JWT è composto da tre parti separate da un punto (.):

  1. Header
  • Specifica il tipo di token (ad esempio: JWT) e l’algoritmo di firma (ad esempio: HS256).
   {
       "alg": "HS256",
       "typ": "JWT"
   }
  1. Payload
  • Contiene i dati (claims) che si desiderano includere nel token in formato JSON.
   {
       "sub": "1234567890",
       "name": "John Doe",
       "admin": true
   }
  1. Signature
  • Firma generata utilizzando la chiave segreta o pubblica, combinando l’header e il payload. Ha lo scopo di prevenire manomissioni.

Principio di funzionamento di JWT

  1. Quando un client invia le credenziali di autenticazione al momento del login, il server genera un JWT basato su queste informazioni.
  2. Il JWT generato viene inviato al client e di solito viene memorizzato in un cookie o nell’intestazione HTTP.
  3. Il client invia il JWT al server con ogni richiesta, e il server verifica il token per autorizzare la richiesta.

Questo processo consente l’autenticazione senza stato, migliorando la scalabilità e la sicurezza.

Scelta della libreria JWT in Python

Esistono diverse librerie in Python per lavorare con i JWT. Ognuna di esse ha delle caratteristiche uniche, e scegliere quella giusta dipende dai requisiti del progetto.

Librerie principali

  1. PyJWT
  • La libreria più comune e leggera. Consente di generare e verificare facilmente i JWT.
  • Sito ufficiale: PyJWT GitHub
  • Caratteristiche:
    • API semplice e facile da usare
    • Sostiene vari algoritmi di firma, tra cui HMAC (HS256, HS512) e RSA (RS256, RS512)
  • Installazione:
    bash pip install pyjwt
  1. Authlib
  • Supporta framework di autenticazione avanzati come OAuth2 e OpenID Connect, e supporta anche i JWT.
  • Sito ufficiale: Authlib
  • Caratteristiche:
    • Funzionalità di sicurezza avanzate
    • Specializzata nella gestione dei token
  • Installazione:
    bash pip install authlib
  1. python-jose
  • Supporta JSON Web Token, JSON Web Signature (JWS), e JSON Web Encryption (JWE).
  • Caratteristiche:
    • Supporto completo per la crittografia (JWE)
    • Personalizzazione avanzata
  • Installazione:
    bash pip install python-jose

Come scegliere la libreria giusta

  • Uso semplice per l’autenticazione: PyJWT è l’ideale. È leggero e intuitivo.
  • Flussi di autenticazione avanzati: Authlib è consigliato se si utilizza OAuth o OpenID Connect.
  • Necessità di crittografia: Se è necessario crittografare i token, python-jose è la scelta migliore.

In base alle dimensioni del progetto e ai requisiti di sicurezza, è possibile utilizzare queste librerie per sfruttare al meglio i JWT.

Implementazione della generazione di JWT in Python

Generare un JWT è semplice ed efficiente con le giuste librerie Python. In questa sezione, ti guideremo attraverso i passaggi per generare un JWT utilizzando la libreria PyJWT.

Generazione di un JWT di base con PyJWT

Il seguente codice mostra come generare un JWT utilizzando PyJWT:

import jwt
import datetime

# Impostazione della chiave segreta
SECRET_KEY = "your-secret-key"

# Creazione del payload
payload = {
    "sub": "1234567890",       # Identificativo utente
    "name": "John Doe",        # Informazioni utente
    "iat": datetime.datetime.utcnow(),  # Ora di emissione del token
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)  # Scadenza (un'ora)
}

# Generazione del token
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")

print("JWT generato:", token)

Spiegazione del codice

  1. Impostazione della chiave segreta
  • Imposta la chiave segreta da usare per firmare il token. Deve essere gestita in modo sicuro all’interno del server.
  1. Creazione del payload
  • Include i claims come sub e name.
  • Può includere l’ora di emissione (iat) e la data di scadenza (exp) se necessario.
  1. Generazione del token
  • Il token viene generato usando jwt.encode, che codifica il payload e genera il token firmato.
  • Gli algoritmi di firma comuni includono HS256 e RS256.

Generazione di un JWT con firma RSA

Di seguito, un esempio di generazione di un JWT utilizzando una firma RSA con una chiave privata e pubblica.

import jwt

# Chiave privata e pubblica RSA
private_key = """-----BEGIN RSA PRIVATE KEY-----
...(contenuto della chiave privata)...
-----END RSA PRIVATE KEY-----"""
public_key = """-----BEGIN PUBLIC KEY-----
...(contenuto della chiave pubblica)...
-----END PUBLIC KEY-----"""

# Creazione del payload
payload = {
    "sub": "1234567890",
    "name": "John Doe",
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}

# Generazione del token con firma RSA
token = jwt.encode(payload, private_key, algorithm="RS256")
print("JWT generato con RSA:", token)

Considerazioni importanti

  1. Gestione della chiave segreta
  • La chiave segreta deve essere gestita in modo sicuro e non deve essere mai esposta a terzi.
  1. Impostazione della scadenza
  • Impostare sempre il campo exp per limitare la durata del token è fondamentale per la sicurezza.
  1. Dimensione del token
  • Includere troppi dati nel payload può aumentare la dimensione del token, riducendo l’efficienza della comunicazione.

Usando questi esempi di codice, è possibile generare JWT adatti alle proprie necessità e costruire un sistema di autenticazione sicuro.

Progettazione dei dati nel payload di JWT

Il payload di JWT contiene informazioni utente o attributi del token. La progettazione del payload è cruciale per l’efficienza e la sicurezza del token.

Composizione di base del payload

Il payload di JWT può contenere tre tipi di “claims” (dichiarazioni):

  1. Claims registrati
  • Claims standardizzati secondo le specifiche. Devono essere utilizzati correttamente.
  • Esempi principali:
    • iss (emittente): chi ha emesso il token.
    • sub (soggetto): il destinatario del token (es. ID utente).
    • aud (destinatario): il destinatario previsto del token (es. nome del servizio).
    • exp (scadenza): scadenza del token in formato timestamp UNIX.
    • iat (emissione): orario di emissione del token in formato timestamp UNIX.
  1. Claims pubblici
  • Può contenere dati personalizzati per l’applicazione.
  • Per evitare conflitti con altri sistemi, utilizzare uno spazio dei nomi (es. namespace/attribute).
  1. Claims privati
  • Contiene dati utilizzati solo all’interno dell’applicazione. Non richiede uno spazio dei nomi.

Best practices nella progettazione del payload

Includere solo i dati necessari


JWT è una stringa codificata in Base64, quindi la dimensione del token influisce sull’efficienza della comunicazione. Assicurati di includere solo i dati essenziali per l’autenticazione e l’autorizzazione.

{
    "sub": "1234567890",
    "name": "John Doe",
    "roles": ["admin", "user"]
}

Considerare la sensibilità dei dati


JWT è protetto dalla firma, ma non è crittografato. Evita di includere informazioni sensibili come:

  • Password
  • Informazioni sulle carte di credito
  • Informazioni personali identificabili (PII: Personally Identifiable Information)

Impostare scadenze chiare per i token


Impostare sempre il claim exp per limitare la durata del token e ridurre i rischi di sicurezza.

Esempio di progettazione del payload di JWT


Ecco un esempio pratico di payload di JWT:

{
    "iss": "https://example.com",   // Emittente
    "sub": "user123",               // ID utente
    "aud": "https://myapi.example.com", // Destinatario
    "exp": 1701209952,              // Scadenza (timestamp UNIX)
    "iat": 1701206352,              // Ora di emissione
    "roles": ["user", "admin"],     // Permessi utente
    "preferences": {
        "theme": "dark",            // Attributi personalizzati
        "notifications": true
    }
}

Considerazioni sulla dimensione del token


Se il payload è troppo grande, la dimensione complessiva del token aumenta e potrebbero sorgere problemi come:

  • Aumento delle dimensioni della richiesta HTTP se inviata tramite intestazioni
  • Aumento dell’utilizzo dei dati nei dispositivi mobili

Riduci la dimensione del token eliminando i claim non necessari.

Riassunto


La progettazione del payload è cruciale per garantire l’efficienza e la sicurezza del token. Utilizzare i claim standard e includere solo le informazioni necessarie per la tua applicazione ti aiuterà a progettare il token ottimale.

Firma con chiave segreta e pubblica

La firma è un elemento cruciale per la sicurezza di JWT. In particolare, l’uso di una firma con chiave pubblica e privata (firma asimmetrica) è efficace per prevenire le manomissioni e garantire l’affidabilità. In questa sezione, spiegheremo come funziona la firma e come implementarla in Python.

Funzionamento della firma

Esistono due tipi di firma in JWT:

  1. Firma simmetrica (HMAC)
  • Utilizza una chiave segreta condivisa per firmare e verificare.
  • Semplice e veloce, ma la gestione della chiave può essere problematica.
  • Algoritmi utilizzati: HS256, HS512
  1. Firma asimmetrica (RSA, ECDSA)
  • La firma viene effettuata con la chiave privata e verificata con la chiave pubblica.
  • Adatta per la comunicazione tra server o con entità terze.
  • Algoritmi utilizzati: RS256, ES256

Vantaggi della firma asimmetrica:

  • Se la chiave privata è ben protetta, la sicurezza è garantita anche se la chiave pubblica viene compromessa.
  • Adatta quando ci sono più validatori.

Implementazione della firma RSA in Python

Di seguito un esempio di come generare e verificare un JWT con firma RSA usando la libreria PyJWT in Python.

Preparazione delle chiavi


Genera prima la chiave privata e quella pubblica.

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

Generazione del JWT


Genera un JWT utilizzando la chiave privata.

import jwt
import datetime

# Caricamento della chiave privata
with open("private.pem", "r") as key_file:
    private_key = key_file.read()

# Creazione del payload
payload = {
    "sub": "1234567890",
    "name": "John Doe",
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}

# Generazione del JWT (firmato)
token = jwt.encode(payload, private_key, algorithm="RS256")
print("JWT generato:", token)

Verifica del JWT


Verifica il JWT utilizzando la chiave pubblica.

# Caricamento della chiave pubblica
with open("public.pem", "r") as key_file:
    public_key = key_file.read()

# Verifica del JWT
try:
    decoded = jwt.decode(token, public_key, algorithms=["RS256"])
    print("Payload decodificato:", decoded)
except jwt.ExpiredSignatureError:
    print("Il token è scaduto.")
except jwt.InvalidTokenError:
    print("Il token non è valido.")

Criteri di scelta dell’algoritmo di firma

  • HS256 (HMAC): per sistemi di autenticazione semplici lato server
  • RS256 (RSA): ideale per comunicazioni tra server o integrazione con sistemi esterni
  • ES256 (ECDSA): utile quando si richiede una firma con chiave pubblica più leggera

Best Practices

  1. Gestione sicura delle chiavi segrete
  • Proteggi rigorosamente la chiave segreta e limita l’accesso ad essa.
  1. Impostazione della scadenza
  • Usa il claim exp per impostare una scadenza breve per il token.
  1. Scelta esplicita dell’algoritmo
  • Specifica sempre esplicitamente l’algoritmo di firma e non fare affidamento su valori predefiniti.

Riassunto


La firma con chiavi segrete e pubbliche è essenziale per garantire la sicurezza di JWT. La firma RSA è particolarmente utile per comunicazioni tra server e sistemi distribuiti. Implementa le best practices per costruire un sistema di autenticazione sicuro e affidabile usando Python.

Processo di verifica di JWT

Il processo di verifica è fondamentale per confermare la validità di un JWT durante l’autenticazione. Verifica che il token non sia stato manomesso, che sia firmato con la chiave corretta e che sia ancora valido. In questa sezione, esploreremo i passaggi per la verifica di JWT in Python.

Processo base di verifica di JWT

  1. Verifica della firma
  • Controlla che il token sia stato firmato correttamente con la chiave privata o pubblica. Se la firma non corrisponde, il token è invalido.
  1. Controllo della scadenza
  • Verifica il claim exp per assicurarti che il token sia ancora valido. I token scaduti sono invalidi.
  1. Verifica di altri claims
  • Verifica claims come iss (emittente) e aud (destinatario) per garantire che il token sia utilizzato correttamente.

Verifica di JWT in Python

Qui sotto mostriamo un esempio di come verificare un JWT usando PyJWT in Python.

Verifica base

import jwt

# Chiave segreta o pubblica
SECRET_KEY = "your-secret-key"

# Token da verificare
token = "your.jwt.token"

# Decodifica e verifica del JWT
try:
    decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    print("Payload verificato:", decoded)
except jwt.ExpiredSignatureError:
    print("Il token è scaduto.")
except jwt.InvalidTokenError:
    print("Il token non è valido.")

Verifica con RS256 (chiave pubblica)

Per una firma asimmetrica (RS256), la verifica avviene utilizzando la chiave pubblica.

# Caricamento della chiave pubblica
with open("public.pem", "r") as key_file:
    public_key = key_file.read()

# Decodifica e verifica del JWT
try:
    decoded = jwt.decode(token, public_key, algorithms=["RS256"])
    print("Payload verificato:", decoded)
except jwt.ExpiredSignatureError:
    print("Il token è scaduto.")
except jwt.InvalidTokenError:
    print("Il token non è valido.")

Verifica di claims aggiuntivi

È possibile rafforzare la verifica specificando claims aggiuntivi, come mostrato nel seguente esempio:

decoded = jwt.decode(
    token,
    SECRET_KEY,
    algorithms=["HS256"],
    options={"verify_exp": True},  # Verifica scadenza
    audience="https://myapi.example.com",  # Verifica destinatario
    issuer="https://example.com"  # Verifica emittente
)

Considerazioni sulla verifica

  1. Specifica dell’algoritmo
  • Assicurati di specificare sempre l’algoritmo utilizzato durante la verifica del token per evitare rischi di sicurezza.
  1. Gestione dei token scaduti
  • I token scaduti (ExpiredSignatureError) devono essere gestiti chiedendo all’utente di effettuare nuovamente l’accesso o di aggiornare il token.
  1. Verifica della fonte del token
  • Assicurati che il token provenga da una fonte affidabile.

Flusso di verifica del token

  1. Ricevi il token
  2. Verifica la struttura del token (Header, Payload, Signature)
  3. Controlla l’algoritmo nel header e verifica la firma
  4. Verifica il claim exp e iss nel payload
  5. Se la verifica ha successo, autorizza la richiesta

Riassunto

La verifica di JWT è essenziale nel processo di autenticazione. Verifica la firma con la chiave corretta e controlla la scadenza per garantire un sistema sicuro e affidabile. Implementa il flusso di verifica del token in Python per un’autenticazione robusta.

Gestione della scadenza del token e del refresh

Quando si utilizzano i JWT, è fondamentale gestire correttamente la scadenza e implementare i refresh token per aumentare la sicurezza. In questa sezione, vediamo come impostare la scadenza e implementare il refresh dei token in Python.

Impostazione della scadenza

Puoi impostare la scadenza del token utilizzando il claim exp. Ecco un esempio di implementazione in Python:

import jwt
import datetime

# Impostazione della chiave segreta
SECRET_KEY = "your-secret-key"

# Aggiunta della scadenza al payload
payload = {
    "sub": "1234567890",       # ID utente
    "name": "John Doe",        # Informazioni utente
    "iat": datetime.datetime.utcnow(),  # Ora di emissione
    "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)  # 30 minuti di validità
}

# Generazione del token
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
print("Access Token:", token)

Considerazioni importanti

  • Imposta una scadenza breve (5-30 minuti) per ridurre i rischi in caso di furto del token.
  • Se il token è scaduto, implementa un sistema di reautenticazione o refresh del token.

Progettazione del refresh token

Il refresh token viene emesso separatamente dall’access token e viene utilizzato per ottenere un nuovo access token quando quest’ultimo scade. Caratteristiche del refresh token:

  1. Ha una durata più lunga (da giorni a settimane)
  2. Di solito viene gestito dal server

Esempio di emessione del refresh token

# Payload del refresh token
refresh_payload = {
    "sub": "1234567890",       # ID utente
    "iat": datetime.datetime.utcnow(),  # Ora di emissione
    "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7)  # 7 giorni di validità
}

# Generazione del refresh token
refresh_token = jwt.encode(refresh_payload, SECRET_KEY, algorithm="HS256")
print("Refresh Token:", refresh_token)

Implementazione del processo di refresh

Quando il token scade, utilizza il refresh token per generare un nuovo access token.

# Decodifica del refresh token e generazione di un nuovo access token
try:
    decoded_refresh = jwt.decode(refresh_token, SECRET_KEY, algorithms=["HS256"])

    # Generazione del nuovo access token
    new_access_payload = {
        "sub": decoded_refresh["sub"],
        "name": "John Doe",
        "iat": datetime.datetime.utcnow(),
        "exp

": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
    }
    new_access_token = jwt.encode(new_access_payload, SECRET_KEY, algorithm="HS256")
    print("Nuovo Access Token:", new_access_token)
except jwt.ExpiredSignatureError:
    print("Il refresh token è scaduto.")
except jwt.InvalidTokenError:
    print("Il refresh token non è valido.")

Considerazioni di sicurezza per il refresh token

  1. Gestione lato server
  • I refresh token devono essere conservati in modo sicuro sul server per prevenire il furto o la manomissione.
  1. Uso una tantum
  • I refresh token devono essere invalidati una volta utilizzati per prevenire attacchi di tipo session hijacking.
  1. Richiesta di reautenticazione
  • Richiedi all’utente di effettuare una nuova autenticazione al momento dell’uso del refresh token per aumentare la sicurezza.

Esempio di progettazione dell’API per il refresh token

Ecco un esempio di progettazione dell’API per il rinnovo del token:

  1. Il client invia un access token scaduto e un refresh token.
  2. Il server verifica il refresh token.
  3. Se valido, il server emette un nuovo access token e lo restituisce al client.

Esempio di endpoint API

POST /api/token/refresh
Authorization: Bearer {refresh_token}

Esempio di risposta:

{
    "access_token": "new_access_token",
    "expires_in": 1800
}

Riassunto

Impostare correttamente la scadenza dei token e implementare il refresh token è fondamentale per garantire la sicurezza e migliorare l’esperienza utente. L’uso combinato di scadenze brevi e refresh token ti permetterà di costruire un sistema di autenticazione sicuro e affidabile.

Rischi di sicurezza e relative contromisure

JWT è un ottimo strumento per l’autenticazione e la gestione delle sessioni, ma ci sono rischi di sicurezza se non implementato correttamente. In questa sezione, discuteremo i rischi più comuni legati ai JWT e le misure per mitigarli.

Rischi di sicurezza comuni

1. Compromissione della chiave segreta


Se la chiave segreta di JWT viene compromessa, è possibile generare token falsificati e accedere al sistema.

Contromisure:

  • Conserva la chiave segreta in un ambiente sicuro (es. variabili di ambiente, servizi di gestione delle chiavi).
  • Ruota regolarmente la chiave segreta.

2. Manomissione del token


Se la firma del token non viene verificata correttamente, è possibile che il contenuto venga manomesso.

Contromisure:

  • Verifica sempre la firma del token.
  • Specifica esplicitamente l’algoritmo e non consentire algoritmi non sicuri (ad esempio none).

3. Furto del token


Se un token viene rubato, l’attaccante può utilizzarlo per accedere come se fosse un utente legittimo.

Contromisure:

  • Usa HTTPS per criptare le comunicazioni.
  • Conserva i token in modo sicuro (es. cookie HttpOnly).
  • Imposta una scadenza breve per i token.

4. Attacco di tipo replay


Se un token viene rubato, può essere riutilizzato da un attaccante per prendere il controllo della sessione.

Contromisure:

  • Aggiungi un identificatore unico (jti) per ogni token e traccia l’uso del token lato server.
  • Inserisci i token utilizzati in una blacklist.

5. Difficoltà di invalidazione


Poiché i JWT sono stateless, non è sempre facile invalidare un token una volta emesso.

Contromisure:

  • Usa refresh token e blacklist per implementare l’invalidazione.
  • Imposta una scadenza breve per i token e aggiorna frequentemente.
  • Implementazione di contromisure specifiche in Python

    1. Gestione delle chiavi


    Un esempio di recupero della chiave segreta da variabili di ambiente:

    import os
    
    SECRET_KEY = os.getenv("JWT_SECRET_KEY")
    if not SECRET_KEY:
        raise ValueError("La chiave segreta deve essere impostata!")

    2. Specifica esplicita dell’algoritmo


    Per prevenire algoritmi non sicuri, specifica sempre l’algoritmo di firma:

    import jwt
    
    # Verifica del token
    decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])

    3. Utilizzo di HTTPS


    Forza HTTPS in Flask:

    from flask import Flask
    
    app = Flask(__name__)
    app.config['PREFERRED_URL_SCHEME'] = 'https'

    4. Prevenzione degli attacchi di replay


    Aggiungi un identificatore unico (jti) per tracciare l’uso del token:

    import uuid
    
    payload = {
        "sub": "1234567890",
        "jti": str(uuid.uuid4()),  # ID unico
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
    }

    5. Implementazione di blacklist


    Un esempio di invalidazione di un token usando Redis:

    import redis
    
    # Client Redis
    redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
    
    # Invalidazione del token
    redis_client.setex("blacklist:" + token, 1800, "true")  # Invalidazione dopo 1800 secondi

    Altri suggerimenti

    • Prevenzione del Cross-Site Scripting (XSS)
      Usa cookie HttpOnly per proteggere i token.
    • Monitoraggio dei log
      Registra i log delle emissioni e verifiche dei token per rilevare utilizzi sospetti.

    Riassunto

    JWT offre numerosi vantaggi, ma richiede misure di sicurezza adeguate. Proteggi le chiavi segrete, verifica le firme dei token e imposta scadenze appropriate per garantire un sistema di autenticazione sicuro.

    Riassunto

    In questo articolo abbiamo esplorato come generare e verificare JWT in Python, spiegando concetti di base, implementazioni e misure di sicurezza. JWT è uno strumento potente per costruire sistemi di autenticazione scalabili e senza stato, ma è fondamentale tenere conto della gestione delle chiavi, della scadenza dei token e della progettazione dei refresh token per garantire la sicurezza del sistema.

    Utilizza le librerie Python per costruire un sistema sicuro ed efficiente che garantisca l’autenticazione degli utenti e la gestione affidabile delle sessioni.

Indice