fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Session has been revoked.

Error message

Session has been revoked.

What it means

Handle throws UnauthorizedException("Session has been revoked.") when the refresh token itself validates but the associated session fails _sessionService.ValidateSessionAsync (hashed token no longer maps to a live session).

Solutions

  1. Send the user through fresh login to create a new session
  2. Don't cache the refresh token beyond the session lifetime
  3. Check session-store retention/TTL configuration if sessions die too early

Example fix

// before
catch (e) { console.error(e); }
// after
catch (e) {
  if (e.message === 'Session has been revoked.') logoutAndRedirectToLogin();
  else throw e;
}
Defensive patterns

Strategy: fallback

Validate before calling

// nothing callable pre-API; ensure you only refresh while the user is marked 'signed in' client-side
if (!store.sessionActive) return; // don't refresh a session you already know is gone

Try / catch

try { return await refreshAsync(token); }
catch (UnauthorizedException e) when (e.Message.Contains("revoked")) { await logout(); redirectToLogin("session-expired"); return null; }

Prevention

When it happens

Trigger: User logged out / admin killed the session; session expired server-side while the client still holds a valid refresh token; session store (DB/Redis) entry purged.

Common situations: Password change or 'sign out all devices' invalidating sessions; long-idle client with revoked session; stale cached client retrying after revocation.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/e4bc414bfb1967ae. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandHandler.cs:65

        // Validate refresh token and rebuild subject + claims
        var validated = await _identityService
            .ValidateRefreshTokenAsync(request.RefreshToken, cancellationToken);

        if (validated is null)
        {
            await _securityAudit.TokenRevokedAsync("unknown", clientId!, "InvalidRefreshToken", cancellationToken);
            throw new UnauthorizedException("Invalid refresh token.");
        }

        var (subject, claims) = validated.Value;

        // Check if the session associated with this refresh token is still valid
        var refreshTokenHash = Sha256Short(request.RefreshToken);
        var isSessionValid = await _sessionService.ValidateSessionAsync(refreshTokenHash, cancellationToken);
        if (!isSessionValid)
        {
            await _securityAudit.TokenRevokedAsync(subject, clientId!, "SessionRevoked", cancellationToken);
            throw new UnauthorizedException("Session has been revoked.");
        }

        // Optionally, cross-check the provided access token subject
        var handler = new JwtSecurityTokenHandler();
        JwtSecurityToken? parsedAccessToken = null;
        try
        {
            parsedAccessToken = handler.ReadJwtToken(request.Token);
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Failed to parse access token during refresh; relying on refresh-token validation only");
        }

        if (parsedAccessToken is not null)
        {
            var accessTokenSubject = parsedAccessToken.Claims
                .FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value

View on GitHub (pinned to 3f2959e683)