{"record":{"id":"ce3694e1a4370f27","repo":"fullstackhero/dotnet-starter-kit","slug":"user-not-found","errorCode":null,"errorMessage":"user not found","messagePattern":"user not found","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/IdentityService.cs","lineNumber":119,"sourceCode":"        return (user.Id, claims);\n    }\n\n    public async Task StoreRefreshTokenAsync(string subject, string refreshToken, DateTime expiresAtUtc, CancellationToken ct = default)\n    {\n        // Targeted UPDATE bypasses tracking + Finbuckle interceptors (which NRE on cross-tenant IgnoreQueryFilters).\n        // Safe: user IDs are globally unique GUIDs, so exactly one row matches Id == subject regardless of tenant.\n        var hashedToken = HashToken(refreshToken);\n        var updated = await _dbContext.Users\n            .IgnoreQueryFilters()\n            .Where(u => u.Id == subject)\n            .ExecuteUpdateAsync(\n                s => s.SetProperty(u => u.RefreshToken, hashedToken)\n                      .SetProperty(u => u.RefreshTokenExpiryTime, expiresAtUtc),\n                ct).ConfigureAwait(false);\n\n        if (updated == 0)\n        {\n            throw new UnauthorizedException(\"user not found\");\n        }\n\n        if (_logger.IsEnabled(LogLevel.Debug))\n        {\n            _logger.LogDebug(\n                \"Stored refresh token for user {UserId}. Token hash: {TokenHash}, Expires: {ExpiresAt}\",\n                subject, hashedToken[..Math.Min(8, hashedToken.Length)], expiresAtUtc);\n        }\n    }\n\n    public async Task<(string Subject, IEnumerable<Claim> Claims)?>\n        BuildClaimsForUserAsync(string userId, string tenantId, CancellationToken ct = default)\n    {\n        ArgumentNullException.ThrowIfNull(userId);\n        ArgumentNullException.ThrowIfNull(tenantId);\n\n        // IgnoreQueryFilters bypasses Finbuckle's tenant filter so root-tenant callers can\n        // resolve users in other tenants during impersonation.","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/IdentityService.cs#L101-L137","documentation":"StoreRefreshTokenAsync performs an ExecuteUpdate on users matching the given id, setting RefreshToken (hash) and expiry. If zero rows were updated the user does not exist (or is filtered out by tenant query filters), so it throws UnauthorizedException('user not found') instead of silently storing nothing.","triggerScenarios":"Storing a refresh token for a userId that was deleted after the access token was issued; a token/subject referencing a user in another tenant; passing a malformed or wrong Guid derived from claims; race where the user is removed between token issue and refresh.","commonSituations":"Refresh flow after an admin deleted the account; multi-tenant deployment where the tenant header changed between login and refresh; identity re-seed wiping users while clients still hold tokens; decrypting a subject string into the wrong id.","solutions":["Verify the userId parsed from the refresh token / claims exists (and in the requesting tenant) before the refresh flow.","Force re-login: the client should treat this 401 as an unrecoverable session and clear stored tokens.","Check tenant resolution on the refresh request matches the tenant the user belongs to.","If users were wiped (re-seed), re-run migrations/seed and have all clients re-authenticate."],"exampleFix":"// before\nconst res = await fetch('/api/v1/auth/refresh', { method: 'POST', body: JSON.stringify({ refreshToken }) });\n// on 401 'user not found' retrying loops forever\n// after\nif (res.status === 401) {\n  clearStoredTokens();\n  redirectToLogin(); // session no longer valid\n}","handlingStrategy":"fallback","validationCode":"const sub = parseJwt(accessToken)?.sub;\nif (!sub || !uuidRegex.test(sub)) { clearTokens(); redirectToLogin(); }","typeGuard":"function hasUsableSession(tokens) {\n  const p = tokens?.accessToken ? parseJwt(tokens.accessToken) : null;\n  return Boolean(p?.sub);\n}","tryCatchPattern":"try { session = await refreshSession(refreshToken); }\ncatch (e) {\n  if (e.status === 401) { clearStoredTokens(); redirectToLogin(); return null; } // 'user not found' is terminal\n  throw e;\n}","preventionTips":["Treat 401 on refresh as terminal — never retry in a loop.","Clear stored tokens when a user is deleted / session is invalid.","Keep tenant headers consistent between login and refresh calls.","After DB re-seeds, invalidate all client sessions and force re-login."],"tags":["authentication","refresh-token","not-found","identity"],"backgroundTag":"user-not-found","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"}