The join model
Three records, each with one authority:- The account: the GIWA-owned record both sign-in methods resolve to.
- The GitHub identity: GitHub’s immutable numeric user ID is the global authority. A mutable GitHub login is display metadata, never an account key.
- The wallet: the global
(chain_id, lower-case address)pair is the authority.
GitHub OAuth
The GitHub flow is hardened against replay, substitution, and account confusion:- PKCE with S256, plus a one-time OAuth
state. Starting the flow creates one hashed, expiring state; only its SHA-256 hash is stored. - The PKCE verifier is derived from the state using a GIWA-only HMAC secret, so neither a database row nor the callback query ever contains the verifier.
- Starting the flow also sets a short-lived, host-only, HttpOnly,
SameSite=Laxcookie containing only the state hash. The callback constant-time compares it against the received state hash and clears it on every outcome. - The callback exchanges the code and consumes the state atomically, then creates or resumes a GIWA session. The exchanged redirect URI is exactly the configured GIWA callback, and a callback for an existing account resumes only its existing GIWA user.
- The expected public origin and callback URI come only from validated GIWA configuration, never from browser-supplied values. A callback never trusts a browser-supplied user ID.
SIWE (Sign-In with Ethereum)
The wallet flow follows EIP-4361 with strict binding:POST /api/auth/siwe/challengecreates one wallet-bound, expiring challenge containing the normalized candidate address before verification. The challenge purpose is bound to eithersign_inorlink_wallet.- The nonce is one-time and bound to the configured origin, URI, chain ID (GIWA Sepolia, 91342), issued time, and expiry.
POST /api/auth/siwe/verifyverifies the proof and consumes the challenge atomically before creating or resuming a session. - Externally owned accounts are verified by ECDSA recovery. Contract wallets are verified via ERC-1271. A wallet type that lacks a tested verifier is rejected.
- A proof with a wrong origin, URI, chain, signature, or ERC-1271 result creates no user, no wallet, and no session.
- Link flows additionally validate the current GIWA session and bind the challenge to that session’s user, so a link flow cannot attach an identity owned by a different GIWA user.
Nothing raw persisted
GitHub access tokens, SIWE signatures, and raw session tokens are used transiently and are never stored or logged. OAuth states and browser session tokens are random values whose SHA-256 hashes are stored; the raw values live only in the browser and the transient exchange. A consumed or expired OAuth state or SIWE nonce cannot create another session. This contract is implemented and tested in the product repository (local localnet). A production GitHub OAuth app and a publicly reachable origin are still ahead of us, tracked on Live Evidence.Why two identities
GitHub carries the builder’s public work persona, and the open-source distribution channel with it. The wallet carries payment and attestation. Joining them under one account, with neither side reassignable, is what lets a Passport say “this human, these wallets, this work” without trusting a self-declared username.Next steps
- For how receipts bind to the wallet side through subject epochs, see Builder Passport.
- For the attestation layer a verified wallet unlocks, see Dojang: On-Chain Attestations.
- For the deployment status of the production GitHub OAuth app, see Live Evidence.