fullstackhero/dotnet-starter-kit · warning · UnauthorizedException
Access token subject mismatch.
Error message
Access token subject mismatch.
What it means
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.
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
Example fix
// before
localStorage.setItem('auth', JSON.stringify(tokens));
// after
localStorage.setItem(`auth:${userId}`, JSON.stringify({ accessToken, refreshToken })); Defensive patterns
Strategy: validation
Validate before calling
const sub = jwtDecode(accessToken)?.sub;
const rSub = jwtDecode(refreshToken)?.sub;
if (sub && rSub && sub !== rSub) { clearTokens(); redirectToLogin(); } Type guard
function tokensMatchPair(accessToken: string, refreshToken: string): boolean {
const a = jwtDecode<{ sub?: string }>(accessToken);
const r = jwtDecode<{ sub?: string }>(refreshToken);
return !a?.sub || !r?.sub || a.sub === r.sub;
} Try / catch
try { return await refreshAsync(refreshToken, accessToken); }
catch (UnauthorizedException e) when (e.Message.Contains("mismatch")) { clearTokens(); redirectToLogin(); return null; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: Multi-account apps swapping localStorage entries; token store not keyed per user; copy/paste of tokens between test users.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/4911bfca870fe32a.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandHandler.cs:90
{
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
?? parsedAccessToken.Subject;
if (!string.IsNullOrEmpty(accessTokenSubject) &&
!string.Equals(accessTokenSubject, subject, StringComparison.Ordinal))
{
await _securityAudit.TokenRevokedAsync(subject, clientId!, "RefreshTokenSubjectMismatch", cancellationToken);
throw new UnauthorizedException("Access token subject mismatch.");
}
}
// Audit previous token revocation by rotation (no raw tokens)
await _securityAudit.TokenRevokedAsync(subject, clientId!, "RefreshTokenRotated", cancellationToken);
// Issue new tokens
var newToken = await _tokenService.IssueAsync(subject, claims, null, cancellationToken);
// Persist rotated refresh token for this user
await _identityService.StoreRefreshTokenAsync(subject, newToken.RefreshToken, newToken.RefreshTokenExpiresAt, cancellationToken);
// Update the session with the new refresh token hash
var newRefreshTokenHash = Sha256Short(newToken.RefreshToken);
await _sessionService.UpdateSessionRefreshTokenAsync(
refreshTokenHash,
newRefreshTokenHash,
newToken.RefreshTokenExpiresAt,View on GitHub (pinned to 3f2959e683)