Expected
The `.cratis-identity` readable cookie the frontend renders "Signed in as ..." from always reflects the currently authenticated caller.
Actual
`IdentityDetailsResolver.Resolve()` has a fast path that skips the full resolution (and the identity endpoint round-trip) whenever the sealed `IdentityAuthorization` cookie is still valid for the current principal:
```csharp
if (mayReuseRecord && authorizationCache.IsAuthorized(context, principal, tenantId))
{
return BuildAuthorizedResult(principal, details: null);
}
```
This path returns correctly (the authorization decision is still checked against the current request's real principal), but it never calls `WriteIdentityState`, which is the only place that rewrites the readable `.cratis-identity` cookie. So whatever was previously written into that cookie - potentially by a completely different person who used the same browser/device earlier, with a longer-lived sealed cookie than the readable one's own MaxAge - stays there and keeps being rendered by the frontend, even though the current request is genuinely, correctly authorized as someone else entirely.
Why it matters
This presents as a real user seeing a different user's name in the UI ("Signed in as woksin" while genuinely signed in and authorized as einari). It reads exactly like a cross-user identity leak and is alarming to find - the actual authorization is unaffected (`InjectIdentityHeadersTransform` and `IdentityMiddleware` both independently rebuild the principal from `HttpContext.User` on every request, never from this cookie or any cache, so the backend always sees the correct caller), but the discrepancy between "authorized as" and "displayed as" is a real, confusing, and easy-to-lose-track-of bug - shared across every application behind AuthProxy, not specific to one.
Suggested direction
Either write the readable identity cookie on the fast path too (from `principal`, even without merged `details`), or bind the readable cookie's own lifetime/validity check to the same criteria the sealed cookie already uses, so the two can't drift apart. State the problem rather than prescribing the fix - there may be a reason `details: null` was chosen for the fast path that a full fix needs to account for.
Reproduction
- Sign in as user A on a given browser/device; readable cookie shows A.
- Sign in as user B on the same browser/device (a different browser profile session, or after A's own sealed-cookie validity window is exercised in a scenario where a different account subsequently becomes the sealed cookie's subject) while the readable cookie's own TTL from step 1 hasn't elapsed.
- Once the sealed cookie is valid for the new session, resolution takes the fast path and the readable cookie is never rewritten - the UI keeps showing whichever identity was last written into it.
Found live in Stagehand production (a Cratis application behind AuthProxy) - a user reported seeing a different, real teammate's GitHub login rendered in the signed-in indicator despite having freshly authenticated as themselves.
Expected
The `.cratis-identity` readable cookie the frontend renders "Signed in as ..." from always reflects the currently authenticated caller.
Actual
`IdentityDetailsResolver.Resolve()` has a fast path that skips the full resolution (and the identity endpoint round-trip) whenever the sealed `IdentityAuthorization` cookie is still valid for the current principal:
```csharp
if (mayReuseRecord && authorizationCache.IsAuthorized(context, principal, tenantId))
{
return BuildAuthorizedResult(principal, details: null);
}
```
This path returns correctly (the authorization decision is still checked against the current request's real principal), but it never calls `WriteIdentityState`, which is the only place that rewrites the readable `.cratis-identity` cookie. So whatever was previously written into that cookie - potentially by a completely different person who used the same browser/device earlier, with a longer-lived sealed cookie than the readable one's own MaxAge - stays there and keeps being rendered by the frontend, even though the current request is genuinely, correctly authorized as someone else entirely.
Why it matters
This presents as a real user seeing a different user's name in the UI ("Signed in as woksin" while genuinely signed in and authorized as einari). It reads exactly like a cross-user identity leak and is alarming to find - the actual authorization is unaffected (`InjectIdentityHeadersTransform` and `IdentityMiddleware` both independently rebuild the principal from `HttpContext.User` on every request, never from this cookie or any cache, so the backend always sees the correct caller), but the discrepancy between "authorized as" and "displayed as" is a real, confusing, and easy-to-lose-track-of bug - shared across every application behind AuthProxy, not specific to one.
Suggested direction
Either write the readable identity cookie on the fast path too (from `principal`, even without merged `details`), or bind the readable cookie's own lifetime/validity check to the same criteria the sealed cookie already uses, so the two can't drift apart. State the problem rather than prescribing the fix - there may be a reason `details: null` was chosen for the fast path that a full fix needs to account for.
Reproduction
Found live in Stagehand production (a Cratis application behind AuthProxy) - a user reported seeing a different, real teammate's GitHub login rendered in the signed-in indicator despite having freshly authenticated as themselves.