README.md: Sentin8 Auth Hosted UI (auth-ui) — Overview
Audience: anyone evaluating or onboarding to the Sentin8:Auth hosted sign-in experience. For wire-level detail read
DESIGN.md; for the business view of what the UI provides readBUSINESS.md.
1. Summary
apps/auth-ui is the Flutter Web application that renders every human-facing
authentication surface of the platform: sign-in, sign-up, MFA, password reset,
email verification, OAuth consent (including FDX-style account selection),
RFC 8628 device approval, CIBA Assisted Login confirmation, and EUDI wallet
sign-in. It is built as a static Flutter Web bundle and embedded directly in
the Sentin8:Auth Rust server, which serves it from the same hostname as the
OAuth endpoints. All API calls are same-origin against the Auth service's own
/v1/* and /oauth/* APIs — the app is the auth web tier, not a BFF client.
2. Motivation
- One identity plane needs one sign-in surface. Sentin8:Auth is the only issuer and authentication authority in the platform (architecture principles §4.1). A hosted, tenant-brandable UI makes that authority visible and consistent for every OAuth client (BFF console, CLI device flow, FDX data-sharing consent, CIBA teller flows) without each client building its own auth screens or ever handling credentials itself.
- Credentials must never leave the auth origin. A same-origin hosted UI
guarantees that passwords and session cookies are exchanged only with the
Auth service itself — clients only ever receive the resulting redirect
(
code/state) at theirredirect_uri. This removes the entire class of token-leak risks that client-side auth forms introduce. - Tenant branding at the front door. Every environment wants its own
logo, colors, and sign-in text. Fetching branding at startup from
GET /v1/branding(cookie-scoped by TLS context headers) lets a single build serve all tenants. - Browser-only, zero-backend build complexity. The app is pure Flutter Web rendered as static files — no server-side rendering, no per-tenant builds, no runtime configuration beyond branding.
3. Architecture & High-Level Design
The Auth Rust server serves both the UI (static files, SPA fallback to
index.html) and the API on one origin. The app renders one page per auth
step, driven by the browser URL; every completed step round-trips the OAuth
context (client_id, redirect_uri, state, code_challenge, scope) to
the next page or back into /oauth/authorize/continue.
- Route-per-step. Each step is a distinct browser path (
/sign-in,/consent,/device, …). Navigation between steps is a full page load, so query parameters carry state and browser history stays aligned with the OAuth redirect model. - Same-origin API.
AuthApiresolves its base URL fromweb.window.location.origin; there is no backend-host configuration at all. - Branding at startup.
BrandingScopeperforms a non-blockingGET /v1/brandingfetch; the UI renders with platform defaults and updates when branding arrives.
4. Key Traits and Data Structures
AuthApi(lib/api.dart): the single HTTP boundary of the app. Encapsulates header policy (content-type,x-sentin8-correlation-id,x-sentin8-csrfwhen a session token id is known), tolerant JSON decoding, and typed exceptions (AuthApiExceptionwithstatusCode/message). Response DTOs (SignInResponse,SignUpResponse,SessionResponse) map the small subset of Auth payloads the UI consumes.Branding/BrandingScope/BrandingProvider(lib/branding_provider.dart): the one cross-widget state holder, anInheritedWidgetsupplying tenant branding to every page.- Pages (
lib/pages/*): one stateful widget per browser route; each owns its local form state and error banner. OAuth context is passed as constructor arguments parsed from the browser URL. Sentin8Colors/Sentin8Text/Sentin8Spacing/sentin8ThemeForBranding()(lib/theme.dart): the design-system layer — 4px grid, Royal Gold accent, dark/light palettes, glass panels, and shared widgets (GlassPanel,Sentin8Logo,ErrorBanner,SuccessBanner,LoadingSpinner).AuthUiLogger/AuthUiMetrics/AuthUiAlerts(lib/observability.dart): client-side observability — a correlation id on every request, an in-memory log ring buffer, per-flow success/latency counters, and sliding-window alert thresholds.registerTestBridge()(lib/test_bridge.dart): e2e automation entry points (window.sentin8TestSignUp/SignIn/Navigate) that invoke the same widget code as the UI buttons — automation never bypasses the UI by calling the API.
5. Rationale, Trade-offs, and Alternatives
- Embed in the Auth server instead of a separate web host. One origin means cookies are first-party, CSRF applies uniformly, and OAuth redirect loops never leave the auth domain. Rejected alternative: serving the UI from Sentin8:Web (as dash is) — that would host a credentials-collecting page under a client-style frontend surface rather than inside the identity service itself.
- Route-per-step with full page loads instead of an SPA router. OAuth
already treats the browser as the state carrier (
state,code_challenge). Fresh page loads make each step restartable, linkable, and trivially aligned with the backend's redirect logic. Rejected alternative: an in-app navigator — it would need to re-implement browser redirect targets (redirect_uri?code=...) as synthetic routes. setState+InheritedWidgetinstead of a state-management package. The app has exactly one shared value (branding); every other state lives inside a single page's lifecycle. Adding Riverpod/Bloc would be overhead without benefit.- Memory-only CSRF token id. The session token id used to arm
x-sentin8-csrfis kept only for the lifetime of the page that fetched it. The durable session lives entirely in the Auth server cookie session. Rejected alternative: persisting tokens in web storage — unnecessary and weaker than the HttpOnly cookie. - Raw
web.window.fetchfor branding,httpfor the API. Branding must be fetched independently ofAuthApiconstruction, always withcredentials: 'include', and must never fail startup; the dedicated API client normalizes everything else. - Flutter CanvasKit + semantics. Enabling
SemanticsBindingproduces real DOM elements with ARIA labels, which is what automated browser testing and assistive technology drive. Trade-off: a slightly larger bundle in exchange for testability and accessibility.
6. Internal Implementation & Invariants (Private Design)
- Invariants:
- Every request the UI issues goes to
window.location.origin— no cross-origin API calls, no configurable backend host. - Password material exists only inside a page's form controllers and the one POST body that consumes it; it is never logged or persisted.
- OAuth context (
client_id,redirect_uri,state,code_challenge,scope) is round-tripped across every navigation between auth pages — losing it would strand the user outside their originating flow. - Sign-up never auto-completes an OAuth authorization; the browser is sent
back through
/oauth/authorize(protected authorization requests) to mint the code. POST /v1/password/reset-requestalways reports success, never revealing whether the email exists.- The CSRF header is armed only from a session token id obtained in the same page session.
- Correlation ids are fresh per browsing session and travel on every request.
- Every request the UI issues goes to
- Browser quirks handled in-code: Flutter's
initialRoutestrips query parameters, so pages re-readwindow.location.search; a deferred session check works around a Flutter-webinitStatesynchronous-HTTP stall; the dart2js RNG avoids1 << 32(which JavaScript wraps). - Consent account selection is fetched only when a
consent_flowparameter is present; the account list arrives pre-selected with All/None controls and is forwarded asselected_account_idson approval.
7. Unresolved Questions / Future Designs
- Sign-out:
AuthApiimplementsPOST /v1/sign-outbut no page invokes it; a sign-out affordance on the hosted UI is not part of any flow yet. - Enterprise SSO (SAML/OIDC) is surfaced as an inactive button stub on the sign-in page.
PasskeyEnrollPage/PasskeyVerifyPageexist in the codebase but are not routed; WebAuthn passkeys are not reachable from the hosted UI today.AccountRecoveryPageis an explainer placeholder; MFA-authenticator recovery is not wired to a backend endpoint yet.- Tenant
customCssis fetched and parsed but not applied; the UI renders with its compiled theme plus branding color/font/logo overrides only. - The consent page does not yet consume the server-side RFW consent UI definition; it renders its own native layout.
Design attribution: sentin8:auth-ui and its underlying concepts were designed by Hamid Jahanzad Hamid@Jahanzad.com.