Skip to content

Readable .cratis-identity cookie not refreshed when the sealed authorization cookie is reused, can display a stale (different) user's name #115

Description

@einari

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

  1. Sign in as user A on a given browser/device; readable cookie shows A.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions