Raxx · internal docs

internal · gated ↑ index

ADR 0042 — Auth Unification: Hybrid Identity Model

Status: Accepted Date: 2026-05-03 Deciders: Kristerpher (product owner), software-architect Scope: All Raxx surfaces — operator and customer-facing Design doc: docs/architecture/auth-unification.md Supersedes: Nothing fully — extends ADR-0001 (passkeys-only applies to customer surfaces), extends ADR-0031 (platform auth posture) Refs: Velvet v2 epic #907; directive 2026-05-03


Context

Raxx operates multiple surfaces. Each had its own ad hoc auth state:

The operator auth story was fragmented: there was no single identity that worked across Console, tickets, and vault. Operators maintained separate credentials for each surface. CF Access provided a network gate but did not supply a unified identity layer. The directive: one login for all operator surfaces, with a transition plan so this is not revisited post-launch.

ADR-0001 established passkeys as the sole auth factor. Its scope was "all user-facing authentication." Operator surfaces evolved independently of that scope. This ADR resolves the gap by establishing a clear surface classification and IDP strategy.


Decision

D1: Google Workspace is the IDP for all operator surfaces

Every operator surface (Console, FreeScout, Infisical vault, internal docs, Velvet admin UI) uses Google Workspace as its identity provider, bound via CF Access Zero Trust. Operators authenticate once with their Google Workspace account (with Workspace-enforced 2FA); CF Access issues a JWT that carries email and groups. Each app layer reads that JWT and maps email → its local identity record.

This is not a violation of ADR-0001 because ADR-0001 scoped its passkey requirement to "user-facing authentication" — i.e. customers authenticating to Antlers. Operator surfaces were always implicitly outside that scope.

D2: Passkeys remain the primary factor for customer-facing Antlers

ADR-0001 and Direction C are unchanged. The customer auth flow on raxx.app and demo.raxx.app uses WebAuthn passkeys as the primary required factor. Google OAuth is an optional second sign-in path for Antlers in Phase 3 only, and only if it can be added without weakening or replacing the passkey path.

D3: Console keeps passkey primary + adds Google OAuth fallback

The Console specifically ships an additional "Sign in with Google" path as a fallback for operators who have lost their passkey device. The passkey path is not retired. Both paths issue the same full session; both write to console_audit_log. The passkey path continues to be the recommended primary path because it is phishing-resistant, origin-scoped, and does not depend on Google's availability.

The google_sub claim (not the Google email; sub is the stable identifier) is stored on the console_admins row as a binding. This is not a credential. It cannot be used to authenticate to Google. No Google token (access, refresh, or ID) is stored.

D4: Workspace 2FA policy is the accepted MFA posture for Google-gated operator surfaces

Rather than implementing per-app TOTP on every operator surface, the Workspace admin enforces strong 2FA on all @raxx.app accounts. CF Access validates the Google IDP session. This meets the intent of ADR-0031's Class 2 / Class 3 MFA requirements through a different mechanism (IDP-enforced factor rather than CF-enforced TOTP).

The Console passkey path still passes through CF Access at the network layer (presence-only), with the phishing-resistant passkey as the app-layer strong factor — consistent with ADR-0031 D3's long-term target.

D5: No auto-provisioning through Google OAuth

Google OAuth on any surface is an auth path, not a registration path. An operator must exist in the app's local identity table before Google OAuth can authenticate them. A valid Google Workspace account that is not pre-provisioned is rejected with 403. This prevents privilege escalation via Google account creation.

D6: hd claim validation is mandatory at every app layer

Every service that receives a Google ID token must validate that the hd (hosted domain) claim equals raxx.app. This is belt-and-suspenders: CF Access already enforces the email domain at the network layer, but an app that skips hd validation creates a path for personal Google accounts to match by email coincidence if the CF policy is ever misconfigured.

D7: PKCE required on every OAuth authorization flow

All OAuth 2.0 flows (Console Google fallback, Velvet admin UI OIDC, FreeScout OAuth module) must use PKCE. Authorization Code without PKCE is forbidden. This is enforced at code-review time and added to the CI security checklist.


Consequences

Positive

Negative / risks

Neutral


Alternatives Considered

Alternative A: Passkeys-only for all surfaces including operators (no Google OAuth)

Rejected for operator surfaces (though this remains the customer-surface goal). The Console passkey bootstrap flow requires a magic-link email per operator per device. Scaling this to FreeScout, Infisical, and internal docs would require building custom passkey auth for each third-party tool, which is infeasible. The ADR-0031 Class 3 designation for FreeScout and vault acknowledges this: they are third-party tools whose auth cannot be replaced without replacing the tool.

For the Console specifically, passkey remains the primary path. Google is fallback, not replacement.

Alternative B: SAML federation through a dedicated IdP (Okta, Auth0, Azure AD)

Rejected at this stage. Raxx runs a one-operator team. Introducing an enterprise IdP adds cost, operational complexity, and another credential surface to protect, for zero user-experience benefit over "sign in with Google Workspace" which all operators already have. SAML readiness is preserved in the RBAC schema (ADR-0020 / rbac-design.md §9) for a future enterprise customer scenario.

Alternative C: CF Access is the only gate; no app-layer auth change

Rejected. CF Access provides network-layer identity and session management but does not supply a per-app identity record. FreeScout and Infisical have their own user tables; without SSO those tables have separate credentials. The directive was "one login for everywhere" — CF Access alone satisfies the network gate but not the app-identity unification.

Alternative D: Google OAuth replaces passkeys on the Console entirely

Rejected as primary path. The passkey is phishing-resistant and origin-scoped. Google OAuth, even with Workspace-enforced 2FA, relies on Google's OIDC endpoint being reachable. Replacing a phishing-resistant factor with a phishable one (OAuth is susceptible to MITM relay attacks if the state/PKCE implementation has gaps) is a security regression. Google is a fallback for device loss recovery, not a replacement for the primary factor.


References