OAuth con PKCE: un verifier riutilizzato annulla parte della protezione

|Autore: Redazione QUASA|6 min di lettura| 1
OAuth con PKCE: un verifier riutilizzato annulla parte della protezione

Con PKCE, il client crea un code_verifier nuovo per ogni richiesta di autorizzazione, calcola il code_challenge con S256 e invia il verifier solo al token endpoint, come prevede la specifica PKCE. Riutilizzare il verifier produce lo stesso challenge per richieste diverse: il confronto crittografico perde la capacità di distinguere la transazione che ha avviato il login.

Per implementare il flusso Authorization Code, client e authorization server devono conservare due legami distinti: il client associa il verifier alla propria richiesta e alla risposta attesa; il server associa challenge e metodo al code emesso. Se un code viene inserito nella risposta di un’altra transazione che usa lo stesso verifier, può superare il controllo PKCE. Altri controlli, tra cui la verifica di state, client e redirect URI, possono ancora respingere la richiesta.

La sequenza verifier–challenge–code–token

Un code_verifier unico genera il challenge S256 associato a una sola richiesta di autorizzazione.

Il code_verifier è una stringa casuale ad alta entropia, lunga da 43 a 128 caratteri ammessi dal protocollo. Una scelta indicata dalla specifica è generare 32 byte con un generatore crittograficamente sicuro e codificarli in base64url senza padding. Il client ricava il challenge con la formula BASE64URL(SHA256(ASCII(code_verifier))). Il verifier originale deve restare disponibile per lo scambio finale, ma non compare nella richiesta di autorizzazione.

  1. All’avvio del login, il client genera un verifier e lo conserva nei dati di quella transazione. Invia all’authorization endpoint code_challenge e code_challenge_method=S256 insieme agli altri parametri della richiesta.
  2. L’authorization server emette un authorization code e vi associa il challenge e il metodo ricevuti. La risposta torna al redirect URI del client.
  3. Il client individua la transazione corrispondente alla risposta e invia code e code_verifier al token endpoint. Il server ricalcola il challenge usando il metodo associato al code e rilascia i token solo se il confronto e gli altri controlli dello scambio riescono.

Uno pseudocodice essenziale per il client è: «verifier = random_sicuro(); challenge = base64url_senza_padding(sha256(ascii(verifier))); salva(transazione, verifier); avvia_autorizzazione(challenge, S256)». Al ritorno: «verifier = recupera(transazione); richiedi_token(code, verifier); elimina(transazione)». Il riferimento alla transazione deve funzionare anche quando lo stesso utente avvia più login contemporaneamente: una variabile globale con l’ultimo verifier generato non basta.

Che cosa cambia quando il verifier viene riutilizzato

Due login simultanei conservano verifier distinti per impedire lo scambio dei rispettivi authorization code.

Un verifier costante nell’applicazione o condiviso da tutte le richieste di una sessione genera sempre lo stesso challenge. In un esempio ipotetico, i login A e B ricevono code diversi ma sono legati allo stesso challenge. Se il client tenta di completare B con il code di A, il verifier di B supera il confronto PKCE anche per A. Il server ha verificato la corrispondenza matematica, ma quella corrispondenza non identifica più il singolo login.

Le best practice di sicurezza OAuth richiedono che il challenge sia specifico della transazione e legato in modo sicuro al client e allo user agent che l’ha avviata. Per i client pubblici richiedono PKCE; per quelli confidenziali lo raccomandano. S256 è il metodo indicato perché, a differenza di plain, non espone il verifier nella richiesta di autorizzazione. Un errore con S256 non giustifica quindi un passaggio automatico a plain.

Il redirect URI richiede anche una difesa contro CSRF. Il client può affidarsi a PKCE a questo scopo dopo aver accertato che l’authorization server lo supporti e lo applichi; altrimenti deve usare un valore state unico, verificato e legato allo user agent, oppure il nonce previsto da OpenID Connect quando pertinente. Il server deve rendere rilevabile il proprio supporto PKCE: può pubblicare code_challenge_methods_supported nei metadata oppure usare un meccanismo specifico del deployment. Se il client parla con più authorization server, deve inoltre ricordare quale server ha avviato ogni richiesta e verificare che la risposta provenga da quello atteso.

I rifiuti necessari al token endpoint

Il token endpoint rifiuta un verifier discordante e un tentativo di scambio senza challenge iniziale.

Il server deve conservare con ogni code il challenge e il metodo ricevuti all’authorization endpoint. Al token endpoint non deve accettare un metodo scelto ex novo dal client: usa quello associato al code. Se il verifier manca, lo scambio protetto da PKCE non può riuscire; se il challenge ricalcolato non coincide, la risposta prevista è invalid_grant. La validità del code, il suo uso singolo e la sua associazione al client restano controlli separati.

Il caso di downgrade richiede un rifiuto ulteriore. Se una richiesta di autorizzazione non conteneva code_challenge, il server deve respingere lo scambio che presenta un code_verifier: ignorarlo farebbe apparire protetto un code emesso senza PKCE. Per un client configurato con PKCE obbligatorio, la richiesta priva di challenge va già respinta all’authorization endpoint. Il metodo S256 dichiarato nella richiesta iniziale, invece, si può verificare rispetto al verifier solo quando quest’ultimo arriva al token endpoint.

I test negativi utili coprono code con verifier mancante, alterato o appartenente a un’altra transazione; code emesso senza challenge e riscattato con un verifier; code già consumato; code destinato a un altro client. Nel test con verifier di un’altra transazione, il risultato atteso è il rifiuto solo se i verifier sono davvero distinti. Se sono stati riutilizzati, quel test può passare: è proprio il difetto che una verifica limitata al confronto PKCE rischia di non rilevare.

Browser pubblico, app nativa e backend confidenziale

Un’applicazione eseguita interamente nel browser non può custodire in modo affidabile un segreto incorporato nel codice distribuito agli utenti. La guida OAuth per le applicazioni browser richiede ai client browser pubblici di usare Authorization Code con PKCE e agli authorization server di supportare e applicare PKCE per quei client. Anche un’app nativa è normalmente un client pubblico: il verifier deve appartenere alla singola richiesta, indipendentemente da dove l’app conserva temporaneamente i dati del login.

Un backend capace di proteggere le proprie credenziali è invece un client confidenziale. Può autenticarsi al token endpoint e usare comunque PKCE contro l’uso improprio di un authorization code. In un’architettura backend for frontend, il backend gestisce lo scambio e conserva i token sul server, associandoli alla sessione del browser; anche qui il verifier resta specifico della richiesta di autorizzazione.

PKCE protegge lo scambio del code, non il successivo impiego dei refresh token. Se ne vengono emessi a un client browser pubblico, l’authorization server deve ruotarli a ogni uso oppure vincolarli al mittente; deve anche fissare una durata massima oppure una scadenza dopo inattività. Questi vincoli limitano l’uso di un refresh token sottratto dopo un login eseguito correttamente.

Leggi anche:

Condividi:

Iscriviti alla nostra newsletter

Ricevi le ultime notizie su Web3, IA e cripto direttamente nella tua casella di posta.

0