Nello scorso articolo, abbiamo visto il meccanismo di funzionamento che sta alla base dello standard JWT. In questo articolo vediamo step by step un esempio completo di autenticazione basato su PHP/MYSQL
Nell’era dei backend API e delle app con architettura a microservizi, è sempre più comune separare le procedure di autenticazione dalle logiche dell’applicazione, disaccoppiando il sistema il più possibile la distribuzione in più server. Se il token JWT viene emesso e convalidato da server separati, il miglior approccio è utilizzare un sistema a chiave pubblica-privata. In questo caso, la chiave privata deve essere archiviata in modo sicuro nel server di autenticazione.
Quindi come applichiamo JWT a un’app PHP? Supponiamo di avere un meccanismo di accesso che utilizza i cookie di sessione per memorizzare le informazioni sullo stato di accesso di un utente all’interno dell’applicazione. Tieni presente che JWT non è stato progettato per sostituire i cookie di sessione. Per questo esempio, avremo un paio di servizi: uno che genera un JWT in base al nome utente e alla password forniti e un altro che recupererà una risorsa protetta a condizione che forniamo un JWT valido.
Supponiamo di avere già il database di supporto in cui memorizzare le credenziali di accesso per ogni account. La tabella degli utenti sarà molto semplice da costruire e conterrà i campi essenziali al riconoscimento dell’utente (id, first_name, last_name, password, email).
Installazione della libreria php-jwt
Procediamo ora con l’installazione di php-jwt utilizzando Composer (che si presuppone sia già installato e funzionante). Sarà necessario eseguire il comando qui sotto in una finestra del terminale (Linux) o del prompt dei comandi (Windows), dalla root del progetto:
$ composer require firebase/php-jwt
Questo comando scaricherà la libreria php-jwt in una cartella chiamata vendor.
Connessione al database
Iniziamo a creare i file di supporto. Per prima cosa ci serve una classe per gestire la connessione al database. Per questo faremo un file php chiamato db.php in cui ci sarà un codice simile a questo:
<?php class DatabaseService { private $db_host = "localhost"; private $db_name = "mydb"; private $db_user = "root"; private $db_password = ""; private $connection; public function getConnection(){ $this->connection = null; try { $this->connection = new PDO("mysql:host=" . $this->db_host . ";dbname=" . $this->db_name, $this->db_user, $this->db_password); } catch(PDOException $exception){ echo "Connection failed: " . $exception->getMessage(); } return $this->connection; } } ?>
L’autenticazione tramite JWT
Creiamo adesso un file chiamato login.php e aggiungiamo il seguente codice per controllare le credenziali dell’utente e restituire un token JWT al client:
<?php include_once 'db.php'; require "vendor/autoload.php"; use \Firebase\JWT\JWT; $email = ''; $password = ''; $databaseService = new DatabaseService(); $conn = $databaseService->getConnection(); $data = json_decode(file_get_contents("php://input")); $email = $data->email; $password = $data->password; $query = "SELECT id, first_name, last_name, password FROM users WHERE email = ? LIMIT 0,1"; $stmt = $conn->prepare( $query ); $stmt->bindParam(1, $email); $stmt->execute(); $num = $stmt->rowCount(); if($num > 0){ $row = $stmt->fetch(PDO::FETCH_ASSOC); $id = $row['id']; $firstname = $row['first_name']; $lastname = $row['last_name']; $password2 = $row['password']; if(password_verify($password, $password2)) { $secret_key = "YOUR_SECRET_KEY"; $issuer_claim = "THE_ISSUER"; $audience_claim = "THE_AUDIENCE"; $issuedat_claim = time(); $notbefore_claim = $issuedat_claim + 10; $expire_claim = $issuedat_claim + 60; $token = array( "iss" => $issuer_claim, "aud" => $audience_claim, "iat" => $issuedat_claim, "nbf" => $notbefore_claim, "exp" => $expire_claim, "data" => array( "id" => $id, "firstname" => $firstname, "lastname" => $lastname, "email" => $email )); http_response_code(200); $jwt = JWT::encode($token, $secret_key); echo json_encode( array( "message" => "Successful login.", "jwt" => $jwt, "email" => $email, "expireAt" => $expire_claim )); } else { http_response_code(401); echo json_encode(array("message" => "Login failed.", "password" => $password)); } } ?>
È possibile definire la struttura dei dati del token come si desidera: solo l’e-mail o l’ID dell’utente o entrambi con qualsiasi informazione aggiuntiva come il nome dell’utente. Ci sono però alcuni claims JWT che dovrebbero essere definiti correttamente perché influenzano il validità del token JWT, ad esempio:
- iat: data e ora dell’emissione del token.
- iss – Una stringa contenente il nome o l’identificatore dell’applicazione emittente. Può essere un nome di dominio e può essere utilizzato per eliminare i token da altre applicazioni.
- nbf – Timestamp di quando il token dovrebbe iniziare a essere considerato valido. Dovrebbe essere uguale o maggiore di iat. In questo caso, il token inizierà ad essere valido dopo 10 secondi dall’emissione
- exp – Timestamp di quando il token dovrebbe smettere di essere valido. Deve essere maggiore di iat e nbf. Nel nostro esempio, il token scadrà dopo 60 secondi dall’emissione.
Nel nostro payload JWT abbiamo aggiunto il nome, il cognome, l’email e l’ID utente dal database. E’ fortemente sconsigliato aggiungere informazioni sensibili nel payload JWT.
Il metodo JWT::encode() trasformerà l’array PHP in formato JSON e firmerà il payload, quindi codificherà il token JWT finale che verrà inviato al client. Nel nostro esempio, abbiamo semplicemente usato una chiave segreta che verrà utilizzata per firmare il payload JWT. In produzione, è necessario assicurarsi di utilizzare una chiave segreta con una stringa binaria lunga e archiviarla in un file di configurazione.
Successivamente, bisogna inviare una richiesta POST al file login.php con un body codificato in JSON contenente l’email e la password di un utente registrato all’interno del database.
Il server risponderà, in caso positivo, con un messaggio JSON contenente la chiave JWT completa. Il token JWT deve essere memorizzato nella cache del browser o nei cookie utilizzando JavaScript, e deve essere allegato a ciascuna richiesta HTTP per accedere a una risorsa protetta sul server PHP.
Accesso a una risorsa protetta tramite JWT
Prima di accedere a un endpoint, il client invia il token JWT con ogni richiesta al server. Il server deve decodificare il JWT e verificare se è valido prima di consentire l’accesso.
Creiamo quindi un file chiamato protetto.php e aggiungiamo il seguente codice:
<?php include_once 'db.php'; require "vendor/autoload.php"; use \Firebase\JWT\JWT; $secret_key = "YOUR_SECRET_KEY"; $jwt = null; $databaseService = new DatabaseService(); $conn = $databaseService->getConnection(); $data = json_decode(file_get_contents("php://input")); $authHeader = $_SERVER['HTTP_AUTHORIZATION']; $arr = explode(" ", $authHeader); $jwt = $arr[1]; if($jwt) { try { $decoded = JWT::decode($jwt, $secret_key, array('HS256')); echo json_encode(array( "message" => "Access granted:", "error" => $e->getMessage() )); } catch (Exception $e) { http_response_code(401); echo json_encode(array( "message" => "Access denied.", "error" => $e->getMessage() )); } } ?>
Il client deve inviare il token JWT all’interno di un’intestazione di autorizzazione HTTP nei formati JWT <token> o Bearer <token>. Per semplificare il tutto, possiamo anche scegliere di includere il token come parametro nell’URL della richiesta o come parte del payload dei dati inviato dal client se non vogliamo gestire le intestazioni HTTP.
Il token inviato al server verrà decodificato. Se il metodo JWT::decode() non restituirà un’eccezione, allora il client verrà autorizzato ad accedere alla risorsa, in caso contrario verrà visualizzato un errore 401 e l’impossibilità ad accedere alla pagina.