La maggior parte dei siti web offre la possibilità di registrarsi per usufruire di servizi. Questo comporta la memorizzazione di credenziali dell'utente. Esso può quindi autenticarsi esibendo le proprie credenziali. È compito del gestore avere cura delle credenziali fornite dall'utente garantendo confidenzialità, integrità e disponibilità dei dati. Questo principio è rafforzato dal Regolamento (UE) 2016/679, conosciuto come GDPR, entrato in vigore in Italia il 25/05/2918.

La definizione di un sistema sicuro non è tuttavia banale. Ci sono accorgimenti da tenere a mente nella realizzazione di un sistema di autenticazione. Questa guida mostra come definire ed implementare in PHP un sistema di registrazione ed autenticazione resistente a SQL injection e brute force. Resta a carico del lettore integrare ulteriori protezioni, ad esempio contro man in the middle o session hijacking.

Questa guida è orientata alla gestione sicura delle credenziali. A titolo di esempio, non si assume né impone l'utilizzo di sessioni per mantenere l'informazione sull'autenticazione (per questo non vengono considerati attacchi session hijacking). Analogamente non si assume che le operazioni debbano avvenire in un contesto web.

Assunzioni
Si assume l'utilizzo di una base di dati per la persistenza. A titolo esemplificativo, il codice fa riferimento all'uso di PDO. I princìpi ed il codice non richiedono un gestore di basi di dati specifico, ma viene fornito uno schema indicativo per MySQL. Si assume che il sistema sia realizzato in PHP, benché gli accorgimenti descritti siano applicabili a qualunque linguaggio.

Si assume che le informazioni necessarie all'autenticazione siano un nominativo univoco (username) ed una parola chiave segreta (password).

Approccio
Occorre definire la struttura di un utente. È necessario memorizzarne lo username, è inoltre ragionevole memorizzarne il momento della creazione (created_at) e dell'ultima modifica (updated_at). Per evitare che un attaccante con accesso alla base di dati possa appropriarsi delle password, queste ultime non sono memorizzare in chiaro. Ne è invece memorizzato un hash, ottenuto grazie a password_hash.
Per mantenere sufficiente generalità, è prevista la possibilità di disattivare un utente senza cancellarlo. Questa funzionalità è utile nel caso si voglia creare un utente inattivo permettendone l'attivazione in un secondo momento, ad esempio previa verifica email. Questa informazione è memorizzata in un attributo active.
Per incrementare la sicurezza, un utente viene disattivato al superamento di una soglia prefissata di tentativi di autenticazione falliti consecutivi. Questo rende necessario memorizzare il numero di tentativi falliti in un attributo consecutive_failed_login_attempts.
Come deterrente per gli attacchi brute force, si impone un'attesa di un lasso di tempo crescente all'aumentare del numero di tentativi falliti consecutivi. Concretamente, questo significa che inserendo una password sbagliata bisognerà attendere un intervallo di tempo via via crescente prima di poter riprovare. A titolo esemplificativo, si considera una funzione polinomiale rispetto al numero di tentativi falliti:
Codice:
waiting_time = consecutive_failed_login_attempts ^ 1.5 - 16
Per realizzare questa funzionalità è necessario tracciare l'ultimo tentativo di autenticazione nell'attributo last_login_attempt.
Per non perdere generalità, si prevede la possibilità di modificare lo username di un utente (purché questo resti univoco). In un'ottica di basi di dati relazionali, ciò è facilmente gestibile utilizzando una chiave surrogata. Per motivi di efficienza è utile che sia possibile accedere rapidamente tramite username, dunque è bene che su questo campo vi sia un indice.

Le precedenti considerazioni sono riassunte nella seguente definizione SQL:
Codice:
CREATE TABLE user(
    id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    username VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    last_login_attempt TIMESTAMP NULL DEFAULT NULL,
    consecutive_failed_login_attempts INT UNSIGNED NULL DEFAULT 0,
    active INT(1) NOT NULL DEFAULT 0,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY(id),
    UNIQUE(username)
);

CREATE INDEX user_username_index ON user(username);
È naturalmente possibile estendere lo schema proposto aggiungendo altri attributi di interesse, ad esempio l'indirizzo email.

Il codice PHP deve essere in grado di creare un utente, verificarne le credenziali e attivarlo/deattivarlo. Alcuni criteri fanno parte del sistema di autenticazione (es. superamento della soglia di tentativi falliti) mentre altri, specifici del dominio applicativo, sono necessariamente a carico del lettore. Per non perdere generalità, le funzionalità sono descritte tramite funzioni, e le operazioni sulla base di dati sono esemplificate tramite PDO. Viene indicato chiaramente, nel codice, dove è necessario provvedere alla connessione. Nella sua forma più semplice, questa può avvenire come:
Codice PHP:
$dbh = new PDO('mysql:host=localhost;dbname=my_database', 'my_user', 'my_password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
La gestione di eventuali errori è demandata alle eccezioni. Per evitare attacchi SQL injection, vengono usati i prepared statement.

Creazione di un utente
La creazione di un utente richiede un username ed una password. In caso di nome utente duplicato, viene sollevata un'eccezione:
Codice PHP:
function user_create($username, $password) {
$password_hash = password_hash($password, PASSWORD_DEFAULT);

// Connects to the database
// $dbh = ...;

// Inserts the new user
$query = 'INSERT INTO user(username, password_hash) VALUES(:username, :password_hash)';
$sth = $dbh->prepare($query);
$sth->execute([
':username' => $username,
':password_hash' => $password_hash
]);
}
Attivazione e disattivazione di un utente
Attivazione e disattivazione richiedono unicamente un username. Nel caso il nome indicato non esista, non accade nulla.
Codice PHP:
function user_activate($username) {
// Connects to the database
// $dbh = ...;

// Sets user as active
$query = 'UPDATE user SET active = 1 WHERE username = :username';
$sth = $dbh->prepare($query);
$sth->execute([':username' => $username]);
}

function
user_deactivate($username) {
// Connects to the database
// $dbh = ...;

// Sets user as active
$query = 'UPDATE user SET active = 0 WHERE username = :username';
$sth = $dbh->prepare($query);
$sth->execute([':username' => $username]);
}
Autenticazione:
L'autenticazione richiede un username ed una password. Restituisce true se e solo se l'utente è correttamente autenticato. L'autenticazione fallisce se almeno una delle seguenti condizioni è verificata:
  • username errato
  • password errata
  • l'utente non è attivo
  • non è trascorso abbastanza tempo dall'ultimo tentativo di autenticazione fallito

Se sono stati effettuati troppi (configurazione globale, esemplificata dalla variabile $config) tentativi falliti consecutivi, l'utente viene disattivato.
Codice PHP:
function user_authenticate($username, $password) {
// Connects to the database
// $dbh = ...;

// Reads global configuration
// $config = ['failed_login_attempts_threshold' => 10];

// Reads data from the database
$query = 'SELECT password_hash, consecutive_failed_login_attempts'
. ' FROM user'
. ' WHERE username = :username AND active = 1 AND (last_login_attempt IS NULL OR (CURRENT_TIMESTAMP - last_login_attempt) > pow(consecutive_failed_login_attempts, 1.5) - 16)';
$sth = $dbh->prepare($query);
$sth->execute([':username' => $username]);
$result = $sth->fetch(PDO::FETCH_ASSOC);

// User is not active or must wait
if ($result === false) {
// Handle this case
return false;
}

// If password is correct, updates last login attempt and exits
if (password_verify($password, $result['password_hash'])) {
$query = 'UPDATE user SET last_login_attempt = CURRENT_TIMESTAMP, consecutive_failed_login_attempts = 0 WHERE username = :username';
$sth = $dbh->prepare($query);
$sth->execute([':username' => $username]);

return
true;
}

// Otherwise, password is incorrect: record the failure
$query = 'UPDATE user'
. ' SET last_login_attempt = CURRENT_TIMESTAMP, consecutive_failed_login_attempts = consecutive_failed_login_attempts + 1'
. ' WHERE username = :username';
$sth = $dbh->prepare($query);
$sth->execute([':username' => $username]);

// Decide whether deactivating the user
if ($result['consecutive_failed_login_attempts'] >= $config['failed_login_attempts_threshold']) {
user_deactivate($username);
}

return
false;
}
Esempi
Codice PHP:
user_create('Alice', 'P455W0RD');
user_activate('Alice');
echo (
user_authenticate('Alice', 'P455W0RD') ? "Logged in" : "Cannot log in");