Docs: Bifrost issues GameCult identity, Heimdall authenticates; oidc-issuer.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-22 18:35:34 +02:00
commit e0640bdaa0
3 changed files with 93 additions and 4 deletions

View file

@ -18,8 +18,10 @@ The product center is the Labor Platform loop: eligible participants express pol
- source of deployment and infrastructure conventions
- documents Yggdrasil, nginx, systemd, and local-Postgres patterns
- `E:\Projects\Heimdall`
- owns OAuth, linked accounts, token custody, signed claims, grants, consent, and revocation
- Bifrost consumes Heimdall identity/capability facts instead of becoming the credential vault
- authenticates: owns OAuth, linked accounts, token custody, signed claims, grants, consent, and revocation
- does not issue GameCult identity: its account id (`HeimdallAccountId`) is an authenticator handle, never who a person is on GameCult
- Bifrost consumes Heimdall authentication/capability facts instead of becoming the credential vault
- remaining scar: Heimdall still mints its own account id in code (`src/app.ts` `createAccount`); chat does not need that to move, so it has not
- `E:\Projects\VoidBot`
- observes Discord, packages consensus, and runs repo Persona heartbeats
- should call Bifrost for governed GitHub/Discord/Reddit/intake crossings instead of becoming hidden transport authority
@ -55,7 +57,7 @@ After bootstrap, the next session should start implementing the first vertical s
- member alpha first
- invite-only onboarding
- GitHub as the only auth provider in v1
- Bifrost is the GameCult identity issuer: `UserAccount.Id` is the subject, and Bifrost is the OIDC provider for chat (`docs/oidc-issuer.md`); GitHub, native, and Heimdall-linked sign-in only authenticate into it
- GitHub issues and internal tasks both supported
- app-native governance and voting
- priority and reward pressure should be transparent and auditable

View file

@ -103,6 +103,8 @@ This keeps Reddit useful without making subreddit mechanics the secret constitut
- signed identity and capability claims
- grants, consent, revocation, and capability evaluation
Heimdall authenticates; it does not issue GameCult identity. Bifrost owns the identity (`UserAccount.Id`, `NormalizedBifrostIdentity`) and issues it to chat as an OIDC provider (`docs/oidc-issuer.md`). `HeimdallAccountId` is an authenticator handle that links a Heimdall account to a Bifrost identity.
Bifrost consumes Heimdall claims. It does not become the key vault. The split is bridge versus gate: Heimdall decides who may cross under which grant; Bifrost moves authorized work across public protocols and records where it landed.
### Patron Collection Crossing
@ -153,7 +155,7 @@ Repo Personas may generate bridge requests. They do not own the bridge itself.
| Concern | Owner | Reason |
| --- | --- | --- |
| Who is this actor? | Heimdall | Identity and account custody must stay with the auth authority. |
| Who is this actor? | Bifrost (identity) with Heimdall (authentication) | Bifrost issues the identity; account custody and credential proof stay with the auth authority. |
| Is this actor allowed to request this crossing? | Heimdall plus Bifrost | Heimdall issues grants; Bifrost applies bridge-domain policy. |
| What work is being requested? | Bifrost | The request shape, priority, provenance, and lifecycle are governance concerns. |
| How does interest become reward pressure? | Bifrost | Priority, bounty growth, vote weight, and reward allocation are Labor Platform concerns. |
@ -205,6 +207,7 @@ This is not the final hosted control plane. It is the current bridge actuator an
- Do not add Bifrost-scoped Discord transport or dispatch receipts to arbitrary repo Persona scripts. Call Bifrost.
- Do not add Bifrost-scoped Reddit posting or Persona flair handling to arbitrary repo Persona scripts. Call Bifrost.
- Do not put OAuth credentials or account custody into Bifrost helper scripts as the long-term plan. Use Heimdall claims.
- Do not hand Heimdall ids, GitHub ids, or display names out as the GameCult subject. The OIDC `sub` is `UserAccount.Id`, and the localpart is `NormalizedBifrostIdentity`, minted once.
- Do not make CultCache documents decide policy. They store packets; Bifrost gives those packets meaning.
- Do not let a repo Persona turn a proposal into endless Aquarium pressure once it has enough shape. Route it to Bifrost as a reviewable artifact.
- Do not expose bridge debug noise to public channels. Public receipts should say what happened, which Bifrost topic or work item owns it, where it landed, and what comes next.

84
docs/oidc-issuer.md Normal file
View file

@ -0,0 +1,84 @@
# Bifrost OIDC issuer
Bifrost is the GameCult identity issuer. It runs an OpenID Connect provider on its public host so that Matrix
Authentication Service (MAS) can sign chat users in with their Bifrost account. Heimdall authenticates people into
Bifrost; it does not issue the identity chat sees.
The provider is OpenIddict (`OpenIddict.AspNetCore` and `OpenIddict.EntityFrameworkCore`, 7.7.1). Bifrost owns two
decisions at the authorize endpoint: who the chat user is, and whether they may get a session.
## Endpoints
The issuer is `Host:PublicBaseUrl`, normalized by .NET into an absolute URI. For a bare host that adds a trailing
slash, so for `https://bifrost.gamecult.org` the issuer string is `https://bifrost.gamecult.org/`. Configure MAS with
that exact string.
| Endpoint | Path |
| --- | --- |
| Discovery | `/.well-known/openid-configuration` |
| JWKS | `/.well-known/jwks` |
| Authorization | `/connect/authorize` |
| Token | `/connect/token` |
| Userinfo | `/connect/userinfo` |
Only the authorization code flow exists, and PKCE (S256) is required. There is no implicit, hybrid, password,
client-credentials or device flow, no refresh token, and no dynamic client registration. The token endpoint takes
`client_secret_basic` only. Scopes are `openid` and `profile`.
## The one client
Exactly one client is registered, from configuration, and seeded or updated at startup. Consent is implicit
(first-party). Any other `client_id`, an unregistered `redirect_uri`, or a request without PKCE is refused by the
provider before any sign-in redirect.
## Configuration
The section is `Bifrost:Oidc`. Absent, the provider surface is not mapped at all and discovery answers 404. Present,
it must be complete: an unreadable or wrong-shaped file stops startup with a message that names the setting and never
echoes its value.
| Setting | Meaning |
| --- | --- |
| `Bifrost:Oidc:SigningKeyPath` | PEM file with an EC P-256 private key. Tokens and id_tokens are signed ES256; JWKS publishes exactly that key. A non-P-256 or public-only key stops startup. |
| `Bifrost:Oidc:EncryptionKeyPath` | File holding 32 random bytes as base64 (`openssl rand -base64 32`). Encrypts the access token and the parked authorize request. |
| `Bifrost:Oidc:Clients:Mas:ClientId` | The client id MAS presents. |
| `Bifrost:Oidc:Clients:Mas:ClientSecretPath` | File holding the client secret. |
| `Bifrost:Oidc:Clients:Mas:RedirectUri` | MAS's callback URL, registered exactly. |
There are no development or ephemeral keys in the host. Key files are provisioned like the other
`[workload.secret_files]` paths.
## Claims
| Claim | Value |
| --- | --- |
| `sub` | `UserAccount.Id`, formatted `D` |
| `preferred_username` | `NormalizedBifrostIdentity` (minted once; there is no rename path) |
| `name` | `DisplayName`, else `BifrostIdentity` |
No e-mail, no Heimdall id, no GitHub id, and no membership detail leaves Bifrost.
## Who may sign in
The authorize endpoint reads the cookie session and the same actor facts the `/App` policy reads.
- No session: challenge to `/auth/sign-in`, which resumes the original request after sign-in. OpenIddict parks the
full request (its request token, in the OpenIddict token table, expiring after ten minutes, spent when used) and
the return path carries only a short `request_uri`, so `LocalReturnPath.MaxLength` stays 256.
- Signed in, membership not `Active`: redirect to the client's `redirect_uri` with `error=access_denied` and
`error_description=The account is not an active GameCult member.`
- Signed in, `Active`, but `NormalizedBifrostIdentity` empty: `error=access_denied` with
`error_description=The account has no Bifrost identity.`
- Signed in and `Active`: a code is issued.
## MAS configuration
Configure MAS's upstream provider with the issuer above, the client id and secret, the token endpoint auth method
`client_secret_basic`, the id_token signing algorithm `ES256`, scope `openid profile`, and a localpart claim import
from `preferred_username`. MAS mints the Matrix localpart on first sign-in, so the claim must never change for an
account.
## Remaining scar
Heimdall still mints its own account id in code (`src/app.ts` `createAccount`). That id is an authenticator handle
and chat does not need it to move.