{"record":{"id":"4911bfca870fe32a","repo":"fullstackhero/dotnet-starter-kit","slug":"access-token-subject-mismatch","errorCode":null,"errorMessage":"Access token subject mismatch.","messagePattern":"Access token subject mismatch\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"warning","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandHandler.cs","lineNumber":90,"sourceCode":"        {\n            parsedAccessToken = handler.ReadJwtToken(request.Token);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogDebug(ex, \"Failed to parse access token during refresh; relying on refresh-token validation only\");\n        }\n\n        if (parsedAccessToken is not null)\n        {\n            var accessTokenSubject = parsedAccessToken.Claims\n                .FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value\n                ?? parsedAccessToken.Subject;\n\n            if (!string.IsNullOrEmpty(accessTokenSubject) &&\n                !string.Equals(accessTokenSubject, subject, StringComparison.Ordinal))\n            {\n                await _securityAudit.TokenRevokedAsync(subject, clientId!, \"RefreshTokenSubjectMismatch\", cancellationToken);\n                throw new UnauthorizedException(\"Access token subject mismatch.\");\n            }\n        }\n\n        // Audit previous token revocation by rotation (no raw tokens)\n        await _securityAudit.TokenRevokedAsync(subject, clientId!, \"RefreshTokenRotated\", cancellationToken);\n\n        // Issue new tokens\n        var newToken = await _tokenService.IssueAsync(subject, claims, null, cancellationToken);\n\n        // Persist rotated refresh token for this user\n        await _identityService.StoreRefreshTokenAsync(subject, newToken.RefreshToken, newToken.RefreshTokenExpiresAt, cancellationToken);\n\n        // Update the session with the new refresh token hash\n        var newRefreshTokenHash = Sha256Short(newToken.RefreshToken);\n        await _sessionService.UpdateSessionRefreshTokenAsync(\n            refreshTokenHash,\n            newRefreshTokenHash,\n            newToken.RefreshTokenExpiresAt,","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandHandler.cs#L72-L108","documentation":"Handle throws UnauthorizedException(\"Access token subject mismatch.\") when the optional access token supplied with the refresh request parses to a subject that does not match (ordinal comparison) the subject embedded in the refresh token — indicating the tokens belong to different users.","triggerScenarios":"Client mixes up tokens across accounts (e.g. stores one global token bag for multiple logged-in users); stale access token from a previous login paired with a new refresh token; corrupted token storage.","commonSituations":"Multi-account apps swapping localStorage entries; token store not keyed per user; copy/paste of tokens between test users.","solutions":["Store access+refresh token pairs keyed by user/session, never globally","If the access token is expired or mismatched, send refresh without it (it is optional)","Clear stored tokens and log in again"],"exampleFix":"// before\nlocalStorage.setItem('auth', JSON.stringify(tokens));\n// after\nlocalStorage.setItem(`auth:${userId}`, JSON.stringify({ accessToken, refreshToken }));","handlingStrategy":"validation","validationCode":"const sub = jwtDecode(accessToken)?.sub;\nconst rSub = jwtDecode(refreshToken)?.sub;\nif (sub && rSub && sub !== rSub) { clearTokens(); redirectToLogin(); }","typeGuard":"function tokensMatchPair(accessToken: string, refreshToken: string): boolean {\n  const a = jwtDecode<{ sub?: string }>(accessToken);\n  const r = jwtDecode<{ sub?: string }>(refreshToken);\n  return !a?.sub || !r?.sub || a.sub === r.sub;\n}","tryCatchPattern":"try { return await refreshAsync(refreshToken, accessToken); }\ncatch (UnauthorizedException e) when (e.Message.Contains(\"mismatch\")) { clearTokens(); redirectToLogin(); return null; }","preventionTips":["Key stored tokens by user id in multi-account apps","Rotate and replace the access+refresh pair atomically on every refresh","Never copy tokens between test accounts or browser profiles"],"tags":["auth","jwt","token-rotation","subject-mismatch"],"backgroundTag":"jwt-subject-mismatch","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}