A pass that cannot read its subjects (a scope factory failing like a database outage) is logged
by exception type only and retried after the interval, never in a tight loop. The earlier loop
test reached this catch through a per-subject exception, which the isolation now absorbs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The named logout HttpClient's primary handler disables auto-redirect, so a 3xx from MAS is a
non-delivery retried like any non-2xx instead of being followed to a final 2xx that revokes.
Pinned through the real DI registration against a real loopback listener answering 302, and
the registered client's Timeout is pinned to exactly 10 s. The unexpected-exception test is
replaced by the isolation test (its fix is the next commit) and the cancellation test stands alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OidcLogoutReconciler derives pending logouts from valid OpenIddict authorizations x
membership, POSTs one signed logout token per non-Active subject to MAS, and revokes
the authorizations only after a 2xx. Registered only when
Bifrost:Oidc:Clients:Mas:BackchannelLogoutUri is set. The active-member reading is
the endpoints' existing one, now shared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One configured client (stale clients removed with their tokens and authorizations), Active membership re-checked at token and userinfo through IsStillActiveMemberAsync, PKCE required by the server option, OpenIddict.Server.AspNetCore + EntityFrameworkCore 7.7.1 with no vulnerable transitives. Soul s2, s3 and s4 hold; Low conformance gaps in gamecult-chat:follow_up:oidc-issuer-spec-conformance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Start takes a free slot or overwrites the one expiring first; parallel starts carry the same request
cookies, choose the same slot and overwrite each other. Complete finds its attempt by scanning the
slots for the posted nonce; an overwritten attempt finds none and is refused like any other. The
per-request counting sweep, its byte budget and the nonce-named cookie are deleted.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Start deletes request attempt cookies that are no live attempt, then the live ones expiring first until the new attempt and the survivors fit 5120 bytes, so the newest attempt always completes and the header stays under nginx's 8 KB. Behavioural tests drive a cookie jar past the bound and read the served page's form fields and action through the test host.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One FixedWindowBudget (injectable clock) owned by ConceptVoteRateLimits;
the vote page 429s with Retry-After before any row or cookie. Prefix
lengths and budgets are validated in every environment. Pins the empty
Origin branch of CastRequestGuard.
Unverified; not yet built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The HeimdallAuthService client redacts x-heimdall-app-secret from HTTP client logging at every level; a Trace canary test pins it.
- The attempt cookie's deletion carries its Path and no Max-Age (which outranks the 1970 expiry); pinned on success and refusal paths.
- LocalReturnPath is at most 256 characters, else /App, so the attempt cookie stays far below the browser limit; pinned at the bound and with the heaviest-escaping path.
- A set BaseUrl with a blank AppSlug is refused at Start before any Heimdall call; behavioural test.
Builds untested until the verification job at the end of this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The key lookup's second cache check after taking the gate changed nothing the refetch throttle did not
already decide, so it is removed. Mutation testing of the diff found the three behaviours above unpinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Start puts a per-attempt nonce in the returnTo it sends Heimdall and keeps the attempt (provider,
member flag, local return path, expiry) in a Data Protection cookie named for that nonce. Heimdall's
completion page sends the browser to /auth/heimdall/complete with a code in the URL fragment; the
page posts it back, Bifrost redeems it at Heimdall, requires the redeemed returnTo to name the
cookie's nonce, verifies the access token for the redeemed account, and signs that browser in.
Deleted: the callback route, the wait route, the server-side attempt store, the pending state and
the backend_callback handoff. Added: LocalReturnPath for return targets, Heimdall:AppSecret sent as
x-heimdall-app-secret (member starts are refused without it), a short timeout on the key fetch, and
a key lookup that does not hold the fetch gate for cached keys. Refusals at the completion route all
answer one redirect and log a fixed reason.
Tests are rewritten against a fake Heimdall with start and redeem routes. Not compile-checked by this
commit alone; later commits record the verification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The forgery and wrong-claim tests now assert which rule refused, start failures,
blank attempt ids, capability shapes and date extremes are covered. A token whose
exp is the largest representable date no longer overflows the clock-skew sum, and
the unreachable blank-return-url fallback is removed (Program defaults it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The verifier was registered as a typed client, which is transient, so each request
would have started with an empty key cache. It is now a singleton that takes its
HTTP client from the factory per fetch. Test hosts share one in-memory database
root so EF keeps a single service provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The callback verifies the EdDSA access token against Heimdall's JWKS and takes the
account, display name and member access from its claims; attempts expire, are
bounded, are completed only if issued, and are redeemed once with a per-attempt
binding cookie. Not yet built or tested; the first verification run follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UseForwardedHeaders runs first with KnownProxies exactly loopback and
ForwardLimit 1; Data Protection always uses application name "Bifrost";
the startup validator refuses production without DataProtection:KeysPath.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>