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>
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>
The account page's POST starts an ordinary sign-in whose attempt (a purpose, the expected
account id, a pending link id; never a token) is only the proof for a link. Each Heimdall
call that needs a bearer follows its own fresh sign-in; the bearer lives in that request.
Google stays out of AllowedProviders and member access. Authorization is redacted from
HttpClient logging. Outcomes redirect to a fixed closed set of wording.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The file wins, is read once when the options are built, and a set-but-missing,
unreadable or empty file stops startup in every environment without naming the
secret. Heimdall__AppSecret still works and logs a deprecation warning.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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 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>
UseConceptTallyCors replaces UseCors and appends Vary: Origin to every
response under the export route, preflights and 404s included, keeping any
Vary already set. A single-origin ASP.NET policy never adds it itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One owner for the route and the policy, so Stryker can mutate them: it
flags every mutant of Program.cs as a compile error (safe mode), which
left the export untested by mutation. Program.cs keeps UseCors and the
two registration calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /public/concepts/{collection}/tallies returns per-live-subject up, down and
member counts only, anonymous, Cache-Control public max-age=60. CORS policy
ConceptTallies is built from ConceptVotes:AllowedOrigins, GET only, no
credentials, required on this route alone. Production startup refuses an
allowed origin that is not an exact https origin.
Not yet built or tested when committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConceptSubjectRegistry is the only writer of ConceptSubject. ConceptKey is the
one key grammar (anchored with \z), shared with ConceptAppraisalService.
Not yet built or tested at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConceptAppraisalService is the only appraisal writer; tallies are derived from
rows. VoterIdentity.For decides voter key and class; NetworkPrefix is the only
prefix computation (HMAC tag). NetworkTagSecret is required in production.
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>