GitHub App Authentication
Overview
xbrain uses a GitHub App for authentication. Sign in once with your GitHub account, and xbrain will read your basic profile, your verified primary email, and the orgs you belong to — nothing more.
The GitHub App architecture lets xbrain (a) call the GitHub API on behalf of your organization with short-lived installation tokens (no long-lived PAT in our env), and (b) keep your sign-in alive for ~6 months without re-authorizing, by transparently refreshing your user token in the background.
Migrated from OAuth (Phase 12)
xbrain previously authenticated via a legacy OAuth application. As of Phase 12, all auth paths use the xbrain GitHub App. The migration is transparent to end-users — sign-in happens at the same URL with the same GitHub consent screen, but the App is what GitHub authorizes (you may be prompted to authorize the new App once).
How sign-in works
The flow is identical for the web app and the Chrome extension:
User clicks "Sign in with GitHub" in xbrain
|
v
GitHub consent screen for the "xbrain" App
|
v
xbrain receives a short OAuth code (~10 min TTL)
|
v
memory-api swaps the code (server-side, client_secret never leaves the VM)
|
v
GitHub returns:
- ghu_... user-to-server access token (~8h TTL)
- ghr_... refresh token (~6 months TTL)
|
v
memory-api persists both tokens encrypted (Fernet) on the user row,
mints an xbt_ session token, returns it to the frontend.
Automatic token refresh
Your ghu_ access token expires after ~8 hours. memory-api detects an
expired token on the next request and silently swaps the refresh token for a fresh
ghu_/ghr_ pair — you stay signed in for ~6 months
without re-authorizing.
Where the tokens live
Both the access token and the refresh token are stored Fernet-encrypted in the
users table (columns github_access_token_enc,
github_refresh_token_enc, plus their expiries and a SHA-256 lookup
hash). They never leave the memory-api process to the browser or any other service.
Org install flow
Some xbrain features (org-membership verification, team auto-grant) need the GitHub App installed on your organization. The first time anyone from your org signs in, xbrain checks whether the App is installed:
- App installed: you land on the teams page with your teams visible and any auto-grant team membership applied.
-
App NOT installed: you see a banner with an
Install xbrain on
<org>button. Clicking it opens GitHub's install screen, where an org admin selects which repos the App can access (the App only requests read-only permissions — see below).
Org admin required
Installing a GitHub App on an organization requires org admin rights. If you are not an admin, GitHub will offer to request the install on your behalf — your admin then approves it from the org settings.
What the App requests (minimal scopes)
| Permission | Access | Why |
|---|---|---|
| Organization → Members | Read | Verify whether you are a member of the org for team-scope routing. |
| Account → Email addresses | Read | Resolve your primary verified email for user identity. |
| Account → Profile | Read | Display name + GitHub login for the team UI. |
No repo access, no write permissions. xbrain cannot create issues, push commits, or read private code from your org via this App.
Installation tokens (server-side)
Beyond user tokens, memory-api mints short-lived installation tokens for backend operations like org-membership checks. The flow:
memory-api needs to call api.github.com on behalf of <org>
|
v
Mint an App JWT (RS256, 10 min TTL) using the App's private key
|
v
POST /app/installations/<inst_id>/access_tokens with the App JWT
|
v
GitHub returns a ghs_ installation token (~1h TTL)
|
v
Cache it in-process for 55 min, then re-mint on demand.
Installation tokens are scoped to the org they were minted for and to the App's
minimal permission set. The App's private key (PEM) lives on the VM as
GITHUB_APP_PRIVATE_KEY_B64 and is never logged or returned.
Install / uninstall webhooks
GitHub notifies xbrain when the App is installed, uninstalled, or its repo selection
changes, via a webhook at POST /v1/webhooks/github/installation.
memory-api verifies the X-Hub-Signature-256 HMAC and updates the
installations table accordingly — no manual sync needed.
Database schema
Phase 12 introduces migration 0019_github_app_install.py:
sql -- migration 0019-- Per-org install records, populated by the install webhook.
CREATE TABLE installations (
id BIGINT PRIMARY KEY, -- GitHub installation_id
org_login VARCHAR(255) NOT NULL,
installed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
removed_at TIMESTAMPTZ
);
-- Per-user encrypted token storage (additions to users).
ALTER TABLE users
ADD COLUMN github_access_token_enc TEXT,
ADD COLUMN github_access_token_exp TIMESTAMPTZ,
ADD COLUMN github_refresh_token_enc TEXT,
ADD COLUMN github_refresh_token_exp TIMESTAMPTZ,
ADD COLUMN github_access_token_hash CHAR(64);
CREATE INDEX users_github_access_token_hash_idx
ON users (github_access_token_hash)
WHERE github_access_token_hash IS NOT NULL;
Session semantics
Sign-in returns an xbt_ session token used by the frontend for all
subsequent xbrain API calls. The ghu_/ghr_ pair stays
server-side; the frontend never sees them. Sign-out invalidates the
xbt_ token but leaves the encrypted tokens in place — the next
sign-in reuses or rotates them depending on expiry.
Security notes
-
Client secret server-only:
GITHUB_APP_CLIENT_SECRETis only ever read on the memory-api VM. It never reaches the browser or the Chrome extension. - Encrypted token storage: both the user access token and the refresh token are Fernet-encrypted in PostgreSQL. Decryption happens in-process only when a request needs to call the GitHub API on the user's behalf.
-
Webhook HMAC: install/uninstall webhooks are authenticated by
HMAC-SHA256 over the raw request body using
GITHUB_APP_WEBHOOK_SECRET. IP allowlisting is intentionally NOT required — HMAC is the authentication boundary. -
State parameter: the OAuth
stateis generated and verified client-side (sessionStorage), preventing CSRF on the callback. - Minimal scopes: the App requests three read-only permissions and nothing else. No repo access, no write to any org or repo.
-
Refresh-token rotation: each refresh issues a new
ghu_/ghr_pair. Old tokens are invalidated by GitHub on rotation; memory-api updates the encrypted row atomically.