fullstackhero/dotnet-starter-kit · error · UnauthorizedException
Invalid refresh token.
Error message
Invalid refresh token.
What it means
RefreshTokenCommandHandler.Handle throws UnauthorizedException("Invalid refresh token.") when _identityService.ValidateRefreshTokenAsync returns null — the presented refresh token is unknown, expired, already used/rotated, or malformed.
Solutions
- Redirect the user to re-login (refresh flow cannot recover an invalid token)
- Prevent concurrent refreshes in the client (single-flight refresh promise)
- Verify signing keys/env config match between token issuance and validation
Example fix
// before
const r = await api.post('/tokens/refresh', { refreshToken: store.token });
// after
refreshPromise ??= api.post('/tokens/refresh', { refreshToken: store.token })
.catch(() => { logout(); });
const r = await refreshPromise; refreshPromise = null; Defensive patterns
Strategy: fallback
Validate before calling
if (string.IsNullOrWhiteSpace(refreshToken)) { logout(); return; }
if (jwtDecode(refreshToken).exp * 1000 < Date.now()) { logout(); return; } // pre-expiry check if token is a readable JWT Type guard
public static bool IsUsableRefreshToken(string? t) => !string.IsNullOrWhiteSpace(t) && t.Length >= 32;
Try / catch
try { return await refreshAsync(token); }
catch (UnauthorizedException) { clearTokens(); redirectToLogin(); return null; } Prevention
- Single-flight all refresh calls behind one shared promise
- Never persist refresh tokens across 'sign out all devices' events
- Keep signing keys and environment consistent between issuing and validating services
When it happens
Trigger: Client sends a refresh token that was already consumed by rotation, an expired token, a token from another environment/issuer key, or garbage/truncated string.
Common situations: Two tabs refreshing concurrently (one rotation wins); client clock skew; switching JWT signing keys or databases; clearing server-side token store while clients hold tokens.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/773d287051033f6d.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandHandler.cs:54
_logger = logger;
}
public async ValueTask<RefreshTokenCommandResponse> Handle(
RefreshTokenCommand request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var clientId = _requestContext.ClientId;
// 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
{View on GitHub (pinned to 3f2959e683)