ADR 0028 — Browser SSO (GitHub/Google) + grounded chat context
Two identity/AI improvements for generated apps:
- a browser SSO login flow (authorization-code, GitHub and Google) alongside
the existing local users and passive OIDC
id_tokenverification; - conversation memory, page grounding, and citation rewriting for the built-in web chat and the vectordb RAG assistant.
1. Browser SSO
Context
Mosaic's identity stack already verifies local JWTs (password login) and, when
declared, OIDC id_tokens against a JWKS/public key. But a public portal has
no browser login path for an external IdP: no sign-in page, no code exchange,
no session cookie. EE's portal uses external SSO for exactly this shape.
Decision
Tessera: identity.sso is a list of providers:
identity:
sso:
- provider: github # github | google
client_id_env: MOAIC_GITHUB_CLIENT_ID
client_secret_env: MOAIC_GITHUB_CLIENT_SECRET
scopes: [read:user, user:email, read:org]
role_map:
default: viewer # fail-closed floor
rules: # first match wins
- role: admin
all:
- claim: login
in: [eugeis]
- role: editor
all:
- claim: orgs
contains: mobility-devops
- provider: google
client_id_env: MOAIC_GOOGLE_CLIENT_ID
client_secret_env: MOAIC_GOOGLE_CLIENT_SECRET
role_map:
default: viewer
rules:
- role: admin
all:
- claim: email
in: [admin@example.com]
- Supported providers are
githubandgoogle; anything else is a plan error (sso-provider-unsupported). Duplicates are rejected. - Secrets are env-var names only (
client_id_envoptional,client_secret_envrequired). The secret itself never appears in the DSL. Missing configuration is a plan error, not a boot surprise. role_map.defaultis required and must be a role known to the app's IAM model. Rules are evaluated in order; the first rule whose predicates all pass wins. Predicates:claim+ exactly one ofeq,in,suffix,domain,contains(array claims supportcontains). A rule may also stamp arealm: {dim: value}onto the issued token.
Generated routes (mounted only when identity.sso is non-empty; the
/auth prefix is public):
| route | behavior |
|---|---|
GET /auth/login | provider button page |
GET /auth/{provider}/login?next=/path | starts the authorization-code flow (UUID state, 300 s TTL, in-memory) |
GET /auth/{provider}/callback | exchanges the code, fetches provider claims, resolves role/realm, issues a local JWT, sets the mosaic_token cookie, redirects to next |
GET /auth/logout | clears the cookie and redirects to /auth/login |
Provider claim fetch:
- GitHub:
/user, verified primary email from/user/emails, org logins from/user/orgs→ claimslogin,id,name,email,orgs[]. - Google: OIDC
/v1/userinfo→ claimssub,email,name, …
The issued JWT carries sub = "{provider}:{provider-subject}", role,
optional realm, email, name, and provider. The auth middleware accepts
the token from Authorization: Bearer … or the mosaic_token cookie
(HttpOnly, SameSite=Lax, Secure under x-forwarded-proto: https).
GET /api/auth/me now returns the verified claims instead of trusting
x-authz-* headers.
Fail-closed runtime states (pinned by examples/authorized e2e): unknown
provider → 404, missing client id → 503, invalid/expired state → 400.
2. Grounded chat context
Context
Both chat surfaces were stateless single-turn calls. EE's agent
(f2843c02a) added page grounding via a ChatContext and a configurable chat
memory window. The mosaic sync note called this "no gap" because mosaic's chat
is generated; this ADR revisits that decision and ports the useful parts.
Decision
Web chat (POST /api/chat, SSR surface) accepts:
{
"message": "…",
"conversation_id": "c-…",
"page_id": "order",
"citations": {"[1]": "https://example.com/a"}
}
The generated chat module keeps an in-memory per-conversation history
(capped at 200 messages) and builds a window from
MOAIC_CHAT_HISTORY_{TURNS,MAX_CHARS} (defaults 10 / 24 000). When
MOAIC_CHAT_HISTORY_SUMMARIZE=true, dropped older turns are summarized via the
configured chat model (MOAIC_CHAT_HISTORY_SUMMARY_MAX_CHARS, default 4000)
and injected as an Earlier conversation summary: system message. A non-empty
page_id adds a page note to the system prompt; citations are deterministic
token→URL rewrites applied to the final answer (LLM and no-LLM fallback). The
chat page persists a conversation_id in localStorage and sends it with each
message. The response includes conversation_id.
Vectordb RAG assistant (POST /api/assistant/chat, app lens) accepts the
same optional fields. Non-streamed and streamed turns now:
- load the conversation history (
MOAIC_ASSISTANT_HISTORY_*, same defaults); - optionally summarize dropped turns;
- retrieve a second
page_sourcesset forpage_idand add[page N]context plus the same page note; - send the full message list (system + history + question) to the LLM;
- rewrite citations in the final answer (LLM, streamed, and grounded no-LLM fallback);
- remember the turn when a
conversation_idwas sent.
The streamed response emits sources, then page_sources, then delta
events, and the final done event carries
{answer, sources, page_sources, llm, conversation_id}.
The e2e scenario assistant_chat_accepts_conversation_page_citations pins the
no-key fail-soft path: the grounded context is returned and the citation token
is rewritten to the caller-supplied URL.
3. mosaic test binary path
The e2e harness spawns the server with the rendered output dir as its working directory. A relative binary path resolved against that cwd, so the harness now canonicalizes the path before spawn.
Consequences
- Public apps get a real browser sign-in/out flow with provider-specific role
mapping, while local users and OIDC
id_tokenverification keep working. - All SSO secrets remain deploy env; the DSL carries only provider names, env var names, scopes, and role rules.
- Chat memory is process-local (in-memory). A multi-replica deployment needs an external store for shared conversations — a follow-up, not part of this ADR.
- Citation rewriting is deterministic string substitution over caller-provided tokens; it is not a semantic doc→URL resolver.
- SSO is intentionally limited to GitHub and Google. A generic OIDC authorization-code flow is a possible extension, but the two concrete providers keep the claim-fetch and role-mapping surface small.