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

  1. Redirect the user to re-login (refresh flow cannot recover an invalid token)
  2. Prevent concurrent refreshes in the client (single-flight refresh promise)
  3. 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

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)