Sovereign Authentication & Authorization Flows
1. OpenIddict as Sovereign Identity Provider (IdP)
Section titled “1. OpenIddict as Sovereign Identity Provider (IdP)”The GERCIA Platform operates OpenIddict running natively inside Gercia.Server as its sole central authority for identity, authorization, and token issuance. It eliminates third-party Identity-as-a-Service (BaaS) vendor lock-in, ensures zero-trust internal token validation, and maintains 100% data residency compliance under LGPD and CFM regulations.
2. The 4 Authentication Flows
Section titled “2. The 4 Authentication Flows”Flow A: Internal GERCIA Staff (ops.gercia.com.br & ops-stg.gercia.com.br)
Section titled “Flow A: Internal GERCIA Staff (ops.gercia.com.br & ops-stg.gercia.com.br)”Internal backoffice operations enforce Zero-Click Authentication governed by Cloudflare Zero Trust Access and Cloudflare Pages Functions:
sequenceDiagram
autonumber
actor Dev as GERCIA Staff Member
participant CF_EDGE as Cloudflare Zero Trust Edge
participant PAGES as Cloudflare Pages (/index.html)
participant FUNC as Pages Function (/api/ops/me)
participant AUTH as Angular AuthService (Signals)
Dev->>CF_EDGE: Accesses ops.gercia.com.br (or ops-stg.gercia.com.br)
Note over Dev,CF_EDGE: Cloudflare Access evaluates WARP Client + Google Workspace SSO (hd=gercia.com.br)
CF_EDGE->>PAGES: Forwards authenticated request with CF-Access-Jwt-Assertion
PAGES-->>Dev: Delivers Angular SPA Bundle (200 OK)
Dev->>AUTH: App Bootstraps (AuthService.initAuth())
AUTH->>FUNC: GET /api/ops/me
FUNC->>FUNC: Validates cf-access-jwt-assertion & cf-access-authenticated-user-email
FUNC-->>AUTH: HTTP 200 { authenticated: true, email: "user@gercia.com.br", ... }
AUTH->>AUTH: Updates state signal: operator = { email, roles }
AUTH-->>Dev: Renders Backoffice Dashboard Instantly (Zero Clicks)
In local developer environments (ops.gercia.dev:7250), AuthService detects loopback development mode and automatically provisions a mock development session (devsecops@gercia.com.br), eliminating Cloudflare Access dependencies during local testing.
Flow B: B2B Multi-Tenant Clients (app.gercia.com.br)
Section titled “Flow B: B2B Multi-Tenant Clients (app.gercia.com.br)”sequenceDiagram
autonumber
actor User as Doctor / Pharmacist
participant SPA as Angular SPA (@gercia/app)
participant Server as Gercia.Server (OpenIddict STS)
participant DB as PostgreSQL (core.users / tenant_user_accesses)
User->>SPA: Submits Email + Password (or selects Enterprise SSO)
SPA->>Server: POST /connect/token (OAuth 2.1 Code Flow with PKCE)
Server->>DB: Verifies Argon2id password hash & TOTP 2FA
Server->>DB: Resolves accessible tenants (TenantId, BranchId, Roles)
Server-->>SPA: Returns Access Token (JWT 15m) + Refresh Token with tenant claims
SPA-->>User: Renders tenant-scoped clinical/pharmacy dashboard
Flow C: Mobile Patients (GERCIA Care)
Section titled “Flow C: Mobile Patients (GERCIA Care)”sequenceDiagram
autonumber
actor Patient as Mobile Patient
participant App as GERCIA Care (KMP)
participant Social as Google / Apple Identity
participant Server as Gercia.Server (/connect/token)
participant DB as PostgreSQL (core.user_logins)
Patient->>App: Clicks "Sign in with Google / Apple"
App->>Social: Prompts biometric authentication on smartphone
Social-->>App: Returns signed id_token
App->>Server: POST /connect/token (grant_type=social_token, id_token=...)
Server->>Server: Validates token signature via native C# (Google.Apis.Auth)
Server->>DB: Finds or provisions User in core.users & core.user_logins
Server-->>App: Emits Access Token (15 min) + Refresh Token (30 days)
App->>App: Stores in hardware SecureStorage (Keychain / Keystore)
Flow D: Hardware Station Agents (Gercia.Edge) & Hybrid Printing
Section titled “Flow D: Hardware Station Agents (Gercia.Edge) & Hybrid Printing”sequenceDiagram
autonumber
participant Browser as Browser (@gercia/app)
participant Edge as Local Gercia.Edge (127.0.0.1:5050)
participant Server as Gercia.Server (Cloud Run)
participant DB as PostgreSQL (core.print_jobs)
rect rgb(240, 248, 255)
Note over Browser,Edge: Bench Mode (Zero Cloud Latency)
Browser->>Edge: GET /v1/scales/stream (CORS verified: app.gercia.com.br)
Edge-->>Browser: Sub-millisecond SSE stream of live weigh telemetry (<1ms)
Browser->>Edge: POST /v1/printers/raw (ZPL byte stream)
Edge-->>Browser: Direct USB raw spooler execution (<50ms)
end
rect rgb(255, 250, 240)
Note over Edge,DB: Centralized Print Server Mode (Outbox Long-Polling)
Edge->>Server: GET /v1/print/jobs/poll (X-Station-Token)
Server->>DB: Validates station token, checks pending jobs
Server-->>Edge: Returns print payload (<200ms) or 204 No Content
Edge->>Server: POST /v1/print/jobs/{id}/complete
end
3. Host-Based Portal Session Segregation (.gercia.ops.session vs .gercia.app.session)
Section titled “3. Host-Based Portal Session Segregation (.gercia.ops.session vs .gercia.app.session)”The enterprise frontend (@gercia/app) serves the internal operations platform (ops.gercia.com.br), the multi-tenant human healthcare & dental portal (app.gercia.com.br), and the sovereign veterinary portal (app.gercia.vet.br). To eliminate cross-portal session pollution and guarantee strict credential isolation even during local development on shared ports (via Cloudflare loopback DNS subdomains ops.gercia.dev, app.gercia.dev, and vet.gercia.dev resolving to 127.0.0.1), portal resolution is strictly host-driven (rejecting query parameter overrides), and authentication schemes are decoupled at the OpenIddict STS gateway level.
flowchart TD
subgraph INGRESS ["1. Strict Host Ingress (Cloudflare DNS)"]
REQ_OPS["Host: ops.gercia.com.br / ops.gercia.dev"]
REQ_APP["Host: app.gercia.com.br / app.gercia.dev"]
REQ_VET["Host: app.gercia.vet.br / vet.gercia.dev"]
end
subgraph ROUTING ["2. Dynamic Policy Scheme Selector"]
SELECTOR["AddPolicyScheme ('GerciaSession')<br>ForwardDefaultSelector(PortalContext.ResolvePortalType)"]
end
subgraph SCHEMES ["3. Isolated Cryptographic Cookie & Token Schemes"]
OPS_COOKIE["GerciaOpsCookie<br>Cookie: .gercia.ops.session<br>Identity: Google Workspace OIDC (@gercia.com.br)<br>Claims: NameIdentifier, Name, picture, OpsAdmin"]
APP_COOKIE["GerciaAppCookie<br>Cookie: .gercia.app.session / Bearer JWT<br>Identity: B2B Sovereign STS (OpenIddict)<br>Claims: NameIdentifier, tenant_id, branch_id, Roles, vertical"]
end
REQ_OPS --> SELECTOR
REQ_APP --> SELECTOR
REQ_VET --> SELECTOR
SELECTOR -- "IsOps == true" --> OPS_COOKIE
SELECTOR -- "IsApp || IsVet == true" --> APP_COOKIE
Architectural Session Invariants:
Section titled “Architectural Session Invariants:”- Dynamic Policy Selector (
GerciaSession): The primary authentication scheme delegates toPortalContext.ResolvePortalTypeat runtime, resolving.gercia.ops.sessionfor operational backoffice requests and.gercia.app.sessionfor B2B client workloads (app.gercia.com.brandapp.gercia.vet.br). - Google OIDC Explicit Binding: Google Workspace OAuth 2.0 configuration sets
SignInScheme = OpsCookieScheme. Successful authentication tickets write strictly to.gercia.ops.session. - Atomic Multi-Scheme Logout: The
/logoutendpoint invokesSignOutAsyncacross bothOpsCookieSchemeandAppCookieSchemesimultaneously, purging all lingering browser session cookies on the active origin. - Permanent Backdoor Elimination: Development bypass endpoints (such as
/login-dev-admin) are strictly forbidden and permanently expurgated from code, guaranteeing zero-trust enforcement across all runtime environments. - Ops Ingress Authorization & Restrictive Access Boundary: Users authenticating via Google Workspace must belong to the corporate domain (
@gercia.com.br) and possess authorized operator roles (governed through theops@gercia.com.brcorporate group). If an authenticated corporate identity lacks active operator privileges, execution is intercepted, suppressing internal navigation bars and presenting a dedicated, styled Restricted Access view instructing the user to request inclusion inops@gercia.com.br. - OIDC PKCE Protocol Standard (
angular-auth-oidc-client): In the Angular SPA, authentication challenges utilize standard OAuth 2.1 Authorization Code Flow with Proof Key for Code Exchange (PKCE). Top-level redirects to external identity providers (Google Workspace OIDC) or sovereign OpenIddict login endpoints are handled cleanly via browser navigation, storing short-lived cryptographic tokens in browser memory with automatic silent token refresh.
4. Defense-in-Depth Against Cookie Tampering, IDOR & Cross-Tenant Intrusion
Section titled “4. Defense-in-Depth Against Cookie Tampering, IDOR & Cross-Tenant Intrusion”The platform rejects perimeter-only authentication models. Even if an attacker manipulates browser storage or crafts malicious payloads, cross-tenant data exfiltration and cross-platform privilege escalation are mathematically prevented across 4 independent security boundaries:
flowchart TD
subgraph ATTACK ["Threat Vectors"]
A1["1. Cookie Payload Tampering<br>(Attempting to inject tenant_id or OpsAdmin)"]
A2["2. Cross-Subdomain Cookie Replay<br>(Renaming .gercia.ops to .gercia.app)"]
A3["3. URL Parameter & Header IDOR<br>(Injecting ?tenant_id=... or X-Tenant-Id)"]
end
subgraph L1 ["Layer 1: Cryptographic Authentication"]
D1["ASP.NET Core Data Protection API<br>HMAC-SHA256 signature + AES-256 encryption<br>Single-bit mutation invalidates MAC instantly"]
end
subgraph L2 ["Layer 2: Sovereign Tenant Resolution"]
D2["WebTenantProvider<br>Reads tenant_id exclusively from decrypted session claims<br>Zero-trust on query strings, route tokens, and headers"]
end
subgraph L3 ["Layer 3: Application ORM Isolation"]
D3["EF Core Global Query Filters<br>Appends WHERE tenant_id = @current_tenant_id<br>Missing claim evaluates to Guid.Empty (0 matches)"]
end
subgraph L4 ["Layer 4: Kernel Database Security"]
D4["Google Cloud SQL Kernel RLS<br>FORCE ROW LEVEL SECURITY<br>USING (tenant_id = current_setting('app.current_tenant_id'))"]
end
A1 -- "HMAC validation failure" --> D1 -- "HTTP 401 Unauthorized" --> BLOCK1["Rejected at Kestrel Gateway 🛑"]
A2 --> D2
A3 --> D2
D2 -- "No tenant_id in Ops cookie" --> D3
D3 --> D4 -- "Cloud SQL drops tuples at disk level" --> BLOCK2["Zero Records Returned (0 Rows) 🔒"]
Threat Mitigation Matrix
Section titled “Threat Mitigation Matrix”| Threat Vector | Targeted Vulnerability | Mitigating Architecture Layer | Result & Security Guarantee |
|---|---|---|---|
| Client-Side Cookie Modification | Tampering with tenant_id or role claims inside browser storage |
Layer 1: ASP.NET Core Data Protection | Cookies are opaque ciphertext signed via HMAC-SHA256. Tampering invalidates the MAC signature; request is rejected with HTTP 401. |
| Cross-Platform Session Replay | Replaying an authenticated Ops cookie against app.gercia.com.br |
Layer 2: WebTenantProvider & core.tenant_user_accesses |
Ops cookie lacks tenant_id claim. TenantId resolves to Guid.Empty. User has no entry in tenant_user_accesses, resulting in 0 rows. |
| Insecure Direct Object Reference (IDOR) | Injecting target clinic ID via ?tenant_id=... or custom header |
Layer 2 & Layer 3: Sovereign Resolution & Query Filters | Ingress parameters are ignored. TenantId is derived strictly from server-signed session claims. |
| Application Query Leak (Missing WHERE) | Developer omission of tenant filter in complex LINQ or raw SQL query | Layer 4: Google Cloud SQL Kernel RLS | PostgreSQL engine enforces FORCE ROW LEVEL SECURITY. Tuples matching another tenant are physically omitted by the kernel query planner. |
| Cross-Site Scripting (XSS) Token Theft | Malicious script attempting to extract active session tokens | Cookie Security Flags | Cookies enforce HttpOnly = true (inaccessible to JavaScript document.cookie) and SameSite = Lax (CSRF prevention). |
| Man-in-the-Middle (MitM) Eavesdropping | Plaintext transport sniffing over public networks | Transport Layer Security | Cloudflare Zero Trust Tunnels enforce TLS 1.3 encryption with Secure = Always cookie flag. |
| Post-Revocation Token Reuse | Stolen session cookie used after employee termination or password reset | Session Expiration & Token Revocation | Ephemeral session lifetimes (8-hour sliding expiration) and immediate revocation of OpenIddict token grants upon credential modification or account deactivation (IsActive = false). |