Commit graph Bifrost/tests/Bifrost.Web.Tests/Support/HeimdallTestKit.cs
Author SHA1 Message Date
Heimdall
6606f8e6e6 Test: merge notice feed and its bearer (red; the feed is a skeleton and the routes do not exist)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-14 10:42:57 +00:00
Heimdall
817363e366 Test: merge page, preview wording and the AccountMerge flag (red; page, preview and confirm words are skeletons)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-14 10:27:22 +00:00
Heimdall
41f4cc3853 Account merge saga: consumer tests through the real host and the fake mover, live-Postgres races, docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-14 06:48:43 +00:00
Heimdall
fbb5853b1d Fake Heimdall mover and consumer-side move client tests
FakeHeimdall.Moves owns per-account logins, a bearer registry, revoked sessions and stored moves,
and answers the identity-moves route in decideMove's order (4fe367a): replay before proofs,
strict passkey coverage, freshness windows, first-receipt replay. Fault switches for status,
apply-then-fail and down.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-14 02:27:38 +00:00
425af74500 Passkeys: WebAuthn enrolment under a fresh sign-in, 48 h cooling-off, assertion proof at unlink (#85)
Delta Soul clean at 73f97ed (no blocking findings; one Low: challenge check and spend not atomic).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-14 00:37:56 +00:00
Heimdall
18f1ec6aca Linked accounts: drop the list after an unlink with an uncertain end; Google never mints; docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 20:23:54 +00:00
Heimdall
8e65f56b0c Linked accounts: never guess a proof provider; require stepUpRequested; no-store on completion
With no current list the proof goes only through a provider the member names, never the
session's sign-in provider: a sign-in through an unlinked provider makes Heimdall mint an
orphan account holding that login. Link buttons need a current list. The unlink completion
must say a step-up was requested. POST /auth/heimdall/complete answers no-store. The fake
Heimdall now models orphan minting, derives step_up_at from the start's stepUp flag, and
enforces the app secret, sign_in-only, unsupported-provider and 600 s rules.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 20:08:07 +00:00
Heimdall
59ecaa606b Linked accounts page: list, link any admitted provider, unlink with step-up proof
A member sees their Heimdall logins, links Discord, Patreon or Google, and unlinks one
with a fresh Google step-up sign-in. Bifrost keeps no Heimdall token: each action is a
fresh Heimdall sign-in started by Bifrost whose bearer lives inside the completion
request. The list is kept five minutes in this process as non-secret fields only.

The Status page's Google-only link button and handler are cut; the Linked accounts page
owns link starts. HeimdallStartRequest gains stepUp. Heimdall's member routes use a
client without the app secret.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 19:35:40 +00:00
Heimdall
1298e7db1e Pin the Google link guards: own slot beside a parallel attempt, undefined purpose, empty pending link id, non-linked 201
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 16:26:25 +00:00
Heimdall
95416653ba Google link: tests and fake Heimdall link routes (red until the flow lands)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 15:21:40 +00:00
Heimdall
87a1bb6b3b Start refusal: bound the body read, survive undecodable bodies, pin the code length cap
Soul pass on c085cead: a refusal with an unsupported charset escaped as a 500, the body was read without a bound, and the 64-character cap was untested.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 14:41:14 +00:00
Heimdall
c757acf525 Make every cookie host-only: __Host- names, Secure, Path=/, no Domain
Session, OAuth correlation, antiforgery, voter and Heimdall attempt cookies all take the
__Host- prefix. The prefix forbids a narrower Path, so the voter (was /mill) and attempt
(was /auth/heimdall) cookies move to Path=/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:42:43 +00: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
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
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
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
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
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