fix(expense): resolve current user id from 'sub' JWT claim

Live verification revealed the JWT carries the user id in the 'sub' claim
(NameClaimType=sub, MapInboundClaims=false), so ClaimTypes.NameIdentifier is
null at runtime. This caused ExpensesController.GetMine/GetById to throw
NullReferenceException (500) on the '!.Value', and made the services fall back
to 'system' — silently defeating the self-ownership guard. Resolve via
NameIdentifier (unit tests) then 'sub' (real tokens). Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-05-29 19:08:21 -07:00
parent 95008788f3
commit e1f99158aa
4 changed files with 45 additions and 6 deletions
+6 -1
View File
@@ -17,8 +17,13 @@ public class ExpenseService : IExpenseService
public ExpenseService(AppDbContext db, IHttpContextAccessor http, IFileStorage storage)
{ _db = db; _http = http; _storage = storage; }
// The JWT carries the user id in the "sub" claim (NameClaimType="sub", MapInboundClaims=false),
// so ClaimTypes.NameIdentifier is absent at runtime. Check NameIdentifier first (unit tests set it),
// then fall back to "sub" (real tokens). Required for the self-ownership guard to work in production.
private string CurrentUserId =>
_http.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";
_http.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier)
?? _http.HttpContext?.User.FindFirstValue("sub")
?? "system";
public async Task<PagedResult<ExpenseListItemDto>> GetPagedAsync(
int page, int pageSize, string? search, int? ministryId,