> ## Documentation Index
> Fetch the complete documentation index at: https://docs.giwa.tokscale.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Dual Identity: GitHub & Wallet

> Learn how GitHub and wallet sign-in resolve to one account, and the replay and substitution defenses on both flows.

One account, two sign-in methods. A user creates an account with GitHub OAuth or with a GIWA-domain SIWE (Sign-In with Ethereum) proof, and both resolve to a single account record. An authenticated user may link the other method later, but neither identity may be reassigned to another user through a sign-in or link flow. This binding is what the [Builder Passport](/builder-passport/overview) is built on.

## 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.

At this foundation stage a user may link multiple wallets but only one GitHub identity; allowing multiple GitHub identities requires a migration.

## 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=Lax` cookie 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/challenge` creates one wallet-bound, expiring challenge containing the normalized candidate address before verification. The challenge purpose is bound to either `sign_in` or `link_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/verify` verifies 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](/overview/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](/builder-passport/overview).
* For the attestation layer a verified wallet unlocks, see [Dojang: On-Chain Attestations](/builder-passport/dojang).
* For the deployment status of the production GitHub OAuth app, see [Live Evidence](/overview/live-evidence).
