← tutti gli articoli

.NET

Cosa ho imparato implementando la JWT authentication in .NET

La scadenza dell'access token è la parte facile da gestire. Il refresh token che scade silenziosamente, senza un percorso di failure esplicito, è ciò che ha davvero causato logout confusi finché non gliene ho dato uno.

Cosa ho imparato implementando la JWT authentication in .NET

Gli access token che scadono sono la parte facile da gestire. Il mio client intercetta un 401, chiama l'endpoint di refresh, ottiene un nuovo access token, e va avanti. La parte che mi frega davvero è il refresh token stesso che scade, silenziosamente, senza niente che dica al client che è successo finché non prova a usarlo e fallisce.

Dove questo si rompe davvero

L'access token ha una durata breve di proposito, quindi ho costruito il flusso di refresh attorno a quello fin dall'inizio. Quello attorno a cui non ho costruito all'inizio è che anche il refresh token ha una propria scadenza, più lunga. Quando quella finisce, la chiamata di refresh stessa fallisce, e se sto gestendo solo il fallimento dell'access token, quel secondo fallimento non ha dove andare. L'utente viene semplicemente sloggato senza un motivo chiaro del perché, nel bel mezzo di qualsiasi cosa stesse facendo.

Cosa sto cambiando

L'endpoint di refresh ha bisogno di un proprio percorso di failure esplicito, separato da un normale 401 su un request ordinario. Se il refresh token è scaduto o non valido, deve instradare da qualche parte di specifico, tornando a una schermata di login con un messaggio vero, non solo un errore di auth generico che sembra identico a qualsiasi altro request fallito.

public async Task<IActionResult> Refresh(RefreshRequest request)
{
    var storedToken = await _tokenStore.FindAsync(request.RefreshToken);

    if (storedToken is null || storedToken.ExpiresAt < DateTimeOffset.UtcNow)
    {
        return Unauthorized(new { reason = "refresh_token_expired" });
    }

    var newAccessToken = _tokenService.CreateAccessToken(storedToken.UserId);
    return Ok(new { accessToken = newAccessToken });
}

Quel campo reason è il pezzo che mi mancava. Senza di esso, il client non ha modo di distinguere "il tuo access token è scaduto, prova a fare refresh" da "il tuo refresh token è morto, fai di nuovo login." Entrambi tornano indietro come lo stesso failure generico altrimenti.

Cosa mi è rimasto davvero da questo

La scadenza del token sono in realtà due eventi separati su due timer diversi, e solo uno di essi ha un percorso di recovery ovvio integrato nel flusso normale. L'altro deve essere gestito di proposito, o compare semplicemente come un logout silenzioso e confuso senza una causa chiara.