{"record":{"id":"773d287051033f6d","repo":"fullstackhero/dotnet-starter-kit","slug":"invalid-refresh-token","errorCode":null,"errorMessage":"Invalid refresh token.","messagePattern":"Invalid refresh token\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandHandler.cs","lineNumber":54,"sourceCode":"        _logger = logger;\n    }\n\n    public async ValueTask<RefreshTokenCommandResponse> Handle(\n        RefreshTokenCommand request,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(request);\n\n        var clientId = _requestContext.ClientId;\n\n        // Validate refresh token and rebuild subject + claims\n        var validated = await _identityService\n            .ValidateRefreshTokenAsync(request.RefreshToken, cancellationToken);\n\n        if (validated is null)\n        {\n            await _securityAudit.TokenRevokedAsync(\"unknown\", clientId!, \"InvalidRefreshToken\", cancellationToken);\n            throw new UnauthorizedException(\"Invalid refresh token.\");\n        }\n\n        var (subject, claims) = validated.Value;\n\n        // Check if the session associated with this refresh token is still valid\n        var refreshTokenHash = Sha256Short(request.RefreshToken);\n        var isSessionValid = await _sessionService.ValidateSessionAsync(refreshTokenHash, cancellationToken);\n        if (!isSessionValid)\n        {\n            await _securityAudit.TokenRevokedAsync(subject, clientId!, \"SessionRevoked\", cancellationToken);\n            throw new UnauthorizedException(\"Session has been revoked.\");\n        }\n\n        // Optionally, cross-check the provided access token subject\n        var handler = new JwtSecurityTokenHandler();\n        JwtSecurityToken? parsedAccessToken = null;\n        try\n        {","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandHandler.cs#L36-L72","documentation":"RefreshTokenCommandHandler.Handle throws UnauthorizedException(\"Invalid refresh token.\") when _identityService.ValidateRefreshTokenAsync returns null — the presented refresh token is unknown, expired, already used/rotated, or malformed.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst r = await api.post('/tokens/refresh', { refreshToken: store.token });\n// after\nrefreshPromise ??= api.post('/tokens/refresh', { refreshToken: store.token })\n  .catch(() => { logout(); });\nconst r = await refreshPromise; refreshPromise = null;","handlingStrategy":"fallback","validationCode":"if (string.IsNullOrWhiteSpace(refreshToken)) { logout(); return; }\nif (jwtDecode(refreshToken).exp * 1000 < Date.now()) { logout(); return; } // pre-expiry check if token is a readable JWT","typeGuard":"public static bool IsUsableRefreshToken(string? t) => !string.IsNullOrWhiteSpace(t) && t.Length >= 32;","tryCatchPattern":"try { return await refreshAsync(token); }\ncatch (UnauthorizedException) { clearTokens(); redirectToLogin(); return null; }","preventionTips":["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"],"tags":["auth","refresh-token","unauthorized"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}