Commit graph

229 commits

Author SHA1 Message Date
Meta Vens
37c8fe1a72 Logout tests: pin the loop's own tick-failure catch now that subject faults are isolated
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>
2026-10-28 01:57:31 +01:00
Meta Vens
8340ee0e92 Logout reconciler: one subject's unexpected exception does not stop the pass
Caught per subject, logged at error with the subject id and exception type only, authorizations
left valid; cancellation still stops the pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:50:54 +01:00
Meta Vens
ec66fba5ca Logout client: no auto-redirect, pin the redirect rule and the 10 s timeout
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>
2026-10-28 01:50:53 +01:00
Meta Vens
51fe807b66 Logout loop test: the tick failure is logged by exception type only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:03:02 +01:00
Meta Vens
868b91fefb Logout reconciler: a recovered subject's backoff clears at once, not when it expires
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:57:31 +01:00
Meta Vens
af64868326 Logout tests: loop, log content, 2xx range, backoff reset, exception and cancellation paths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:56:18 +01:00
Meta Vens
c9e470bef4 docs/oidc-issuer: BackchannelLogoutUri, reconcile semantics, MAS facts and the tick-window tradeoff
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:39:57 +01:00
Meta Vens
69b670e3af Fix logout test support compile errors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:38:56 +01:00
Meta Vens
404dafdd40 OIDC back-channel logout: a reconciler ends the sessions of subjects who are not Active
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>
2026-10-28 00:38:15 +01:00
Meta Vens
83daabe512 Bifrost as OIDC issuer for the Matrix chat (gamecult-chat cut bifrost-oidc-issuer, squash of 1ef289a..ec4acfa and abd12b3..)
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>
2026-10-27 04:57:16 +01:00
Meta Vens
78d38d92e6 Merge security/bifrost-dependency-pins: Npgsql 8.0.3, System.Text.Json 8.0.5, Caching.Memory 8.0.1 pinned; NuGet lockfiles; locked-mode restore (audit 2026-10-22; Soul cleared)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:45:01 +02:00
Meta Vens
d7d4b21c4b Commit NuGet lockfiles for the web and test projects
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:29:46 +02:00
Meta Vens
a15648db04 Pin Npgsql 8.0.3, System.Text.Json 8.0.5, Caching.Memory 8.0.1; enable NuGet lockfiles
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:26:49 +02:00
Meta Vens
b2110f53fa Merge hands/heimdall-callback-signed
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 18:11:28 +02:00
Meta Vens
b7e3bdf28d Heimdall sign-in: the attempt reader takes no optional nonce; ownership is one ordinal comparison
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 23:02:48 +02:00
Meta Vens
d743e4f363 Heimdall tests: an unreadable form with a genuine attempt cookie present is still no attempt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 23:00:06 +02:00
Meta Vens
efb278a910 Heimdall tests: the no-attempt-field refusal carries a genuine code, so only the missing nonce can refuse it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:57:26 +02:00
Meta Vens
d463336206 Heimdall tests: a genuine attempt cookie under a name that is no slot is no attempt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:41:31 +02:00
Meta Vens
b54e8d5dc8 Heimdall tests: fix a string literal in the parallel-starts test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:32:07 +02:00
Meta Vens
053272a0f3 Heimdall tests: re-point the cookie-ring tests to the slot cookie; a refusal with no matching attempt deletes nothing
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:31:25 +02:00
Meta Vens
15f96c408c Heimdall sign-in: attempt cookies live in a ring of two fixed slots, so the Cookie header is bounded by construction; pin the completion form's id
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>
2026-10-11 22:25:24 +02:00
Meta Vens
8186d6f8fa Heimdall tests: drop the exact-byte boundary test; the 5120-byte bound has a 3 KB margin, and no two attempt cookies can reach it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:33:17 +02:00
Meta Vens
21f2c75c78 Heimdall tests: name the sums near the bound when the boundary pair is missing
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:32:31 +02:00
Meta Vens
ec2f812b59 Heimdall attempt cookie bound: pin the exact byte boundary and that unrelated cookies are never swept
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:26:31 +02:00
Meta Vens
4a28cea1f9 Heimdall sign-in: Start sweeps dead and oldest attempt cookies to keep the Cookie header bounded; pin the complete page's real form
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>
2026-10-11 20:41:41 +02:00
Meta Vens
79c29fe16e Merge hands/mill-votes-cut-5b: vote budgets per prefix and account, mint budget 30 per address per hour, monotonic windows, oldest-unexhausted eviction at the cap
Soul passes concept-mill-votes:verdict:cut-5b.s1 to .s4. Operator rulings
mint-budget-30-per-hour and budget-table-evict-oldest. Undeployed; Cut 6
deploys with the sign-in fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:35:34 +02:00
Meta Vens
cf74019194 Mill votes: the very-busy refusal offers sign-in to a voter who is not signed in
Accounts have their own table, so signing in works while the cast or mint
table is full. A signed-in voter is not told to sign in. Pinned through the
full pipeline at a small cap, for both audiences.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 20:23:59 +02:00
Meta Vens
a9b9b29bf9 Pin the account table's own cap
The cap-not-wired-accounts hand mutant survived: no test filled the account table.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:58:09 +02:00
Meta Vens
912d083894 Fix a string literal broken across lines in the table-full test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:17:27 +02:00
Meta Vens
436a02864f Vote budgets: at the cap a new key evicts the least recently used key with permits left
Ruling budget-table-evict-oldest. FixedWindowBudget keeps two linked lists (window start, last use among keys
with permits left) so eviction and expiry are O(1); a spent key is never evicted, and a table full of spent keys
refuses a new key as TableFull with the true wait. Accounts get their own cast table. The vote page checks the
subject before spending any key, and the refusal names its reason: cast budget, mint budget, or a busy table.
Not verified by a build yet at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:16:30 +02:00
Meta Vens
8f57dc89f0 Pin the no-client-address bucket of the vote budgets
Kills the Stryker survivor on the null-client key: a request with no address shares one
bucket and does not throw. Builds with a8c3ecf; test added after the main verify job.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 17:38:56 +02:00
Meta Vens
a8c3ecf495 Vote budgets: monotonic windows, a key cap, 30 mints an hour and a readable 429
FixedWindowBudget times windows on the clock's monotonic timestamps so a wall-clock
step cannot stretch or reset one, expires windows from a start-ordered queue instead
of a periodic sweep, and holds at most MaxTrackedKeys keys: a new key at the cap is
denied with the true wait and a held key is never evicted. MintPermits is 30 per hour
per the operator ruling, and a 429 now carries a plain-text reason; a refused new
device is told it can sign in. Not yet built or tested at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 17:32:01 +02:00
Meta Vens
2c8384e44a Pin that a signed-in retract spends the account budget
Hand mutant retract-key-ignores-signed-in-voter survived the first table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:46:45 +02:00
Meta Vens
9fcef927fd Pin the budget sweep and Retry-After rounding; drop a redundant Math.Max
Survivors of the first Stryker pass on FixedWindowBudget: the sweep dropping
live windows, sweeping every call, and the exact window boundary (masked by
the sweep in the first test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:35:50 +02:00
Meta Vens
34f5dfa6f1 Heimdall attempt cookie: a deletion carries only the Path
HttpOnly, Secure and SameSite on a deletion decide nothing (a cookie is matched on name and path); Stryker found those two booleans surviving. The deletion is pinned by path, expiry and the absence of Max-Age.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:08:10 +02:00
Meta Vens
7f375ecf8f Pin the empty Origin branch with a server-built request
HttpClient drops an empty header value, so the first version of the test
never sent the header; the request is now built on the TestServer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:02:01 +02:00
Meta Vens
0f71a9f3ca Heimdall tests: fix two log-join string literals broken by a newline
d695674 does not build (two string literals split across lines); this commit repairs it. cf4e5b4 inherits the break.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:01:06 +02:00
Meta Vens
fbf6e43955 Concept vote rate limits: cast and mint budgets per account or network prefix
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>
2026-10-11 09:00:23 +02:00
Meta Vens
cf4e5b4327 Heimdall complete page: run its real script under Node in tests
A committed harness (Fixtures/complete-page-harness.cjs) executes the served page's inline script against a simulated browser; tests pin that the fragment is dropped before the post, an error status never submits, and the code is read only from the fragment. Node is required on PATH; a missing Node fails, it does not skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:58:20 +02:00
Meta Vens
d695674359 Heimdall sign-in: redact the app secret from client logging, expire the attempt cookie at its path, bound the return path
- 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>
2026-10-11 08:58:19 +02:00
Meta Vens
f5432f7a42 Heimdall sign-in: pin the public base URL path, the nonce shape and the Discord role requirement; drop a redundant key re-check
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>
2026-10-11 05:25:14 +02:00
Meta Vens
ad06e2ffd5 Heimdall completion route: no antiforgery opt-out (the form is read by hand); pin a malformed form body
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:43:45 +02:00
Meta Vens
713eff934a Heimdall tests: register the GitHub OAuth scheme through host settings so the sign-in return path can be read from its state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:42:50 +02:00
Meta Vens
5626532571 Heimdall sign-in: the start result carries the cookie name; fix a test string literal
The commit before this one did not compile (a split string literal in the tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:41:36 +02:00
Meta Vens
bad2117ea8 Heimdall sign-in: the attempt is a cookie, and a code the browser presents is redeemed at Heimdall
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>
2026-10-11 04:40:47 +02:00
Meta Vens
1ab67da47b Heimdall sign-in tests: pin each refusal rule by its reason; token dates cannot overflow
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>
2026-10-11 03:26:48 +02:00
Meta Vens
650bf6bcc6 Heimdall token verifier: split decoding and key reading into small steps
Behaviour is unchanged. The catch-and-refuse blocks sit in their own small
methods so the mutation tool can compile mutants of the rest of the verifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:05:07 +02:00
Meta Vens
e99eea8769 Heimdall token verifier: one process-wide instance owns the key cache
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>
2026-10-11 02:55:35 +02:00
Meta Vens
f3b8acbbc5 Heimdall sign-in: accept only a Heimdall-signed token for an attempt Start issued, in the browser that started it
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>
2026-10-11 02:54:13 +02:00
Meta Vens
68214ced30 Merge hands/mill-votes-cut-5: the vote page, the device-cookie voter key and cookieless CSRF
Held by concept-mill-votes:verdict:cut-5.s2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:54:41 +02:00