Skip to content

Auth & Security

Olly uses Keycloak as its OpenID Connect (OIDC) and SAML 2.0 identity provider, deployed self-hosted on EKS in the private subnet. All authentication flows use PKCE (Authorization Code with Proof Key for Code Exchange) — the OAuth 2.0 implicit flow is not used anywhere. Internal machine-to-machine calls use Client Credentials with secrets rotated every 24 hours by OpenBao.

Keycloak is never exposed directly to the internet. APISIX proxies the /auth/ path for external clients. All internal services validate JWTs using the Keycloak JWKS endpoint, which is cached in service memory with a 5-minute TTL.

Realm Structure

The platform uses four Keycloak realms to enforce hard isolation between user populations. A token issued in one realm is invalid in any other realm's context.

RealmUsersClientsAuth FlowMFA
olly-membersIndividual members, employer HR adminsolly-mobile, olly-member-portal, olly-employer-portalPKCE Authorization CodeOptional for members; required for EMPLOYER_ADMIN
olly-providersLicensed providers (NPI holders), billing adminsolly-provider-portalPKCE Authorization CodeRequired for all provider users (TOTP)
olly-internalOlly operations staff, claims adjusters, underwriters, system adminsolly-admin-consolePKCE Authorization CodeRequired for all users (TOTP; FIDO2/passkeys in P3)
olly-servicesNone — M2M onlyOne client per Go serviceClient CredentialsN/A (client secrets + mTLS)

Why four realms? A compromised member token cannot be used in the internal admin context. Each realm enforces independent session policies, MFA requirements, and IP restrictions. The operational overhead is justified by the security boundary enforcement.

Auth Flow per Client Type

Client TypeRealmPKCEToken StorageSession Enforcer
Mobile app (Expo RN)olly-membersYes (expo-auth-session, RFC 7636)Expo SecureStore (iOS Keychain / Android Keystore)Biometric gate on SecureStore
Web portals (Next.js)olly-members / olly-providersYes (next-auth v5)httpOnly Secure SameSite=Strict cookie; token never in JSnext-auth server-side session
Admin console (React SPA)olly-internalYes (via thin Next.js BFF)httpOnly Secure cookie set by BFFBFF session check + OPA IP restriction
Go services (M2M)olly-servicesNo (Client Credentials)In-memory with TTL buffer; sourced from OpenBaomTLS between all service pods

Mobile PKCE Flow (Expo React Native)

The app generates a code_verifier (43–128 random chars) and code_challenge (SHA-256, base64url), redirects to Keycloak, and exchanges the authorization code for tokens. Access tokens are stored exclusively in Expo SecureStore — never in AsyncStorage, memory logs, or JavaScript variables. Biometric verification (Face ID / Touch ID via Expo LocalAuthentication) gates access to SecureStore on every app open and before sensitive actions (claim submission, EOB viewing). Biometric checks are local-only and do not involve a server round-trip.

Web Portal BFF Pattern (Next.js)

next-auth v5 handles the PKCE exchange server-side and stores the session in an httpOnly, Secure, SameSite=Strict cookie. Client-side JavaScript never sees a raw JWT. Next.js Server Components and Route Handlers use auth() to read the session; client components call Next.js API routes that attach the Bearer token to upstream service calls. Silent token refresh happens transparently via next-auth before access token expiry.

Service-to-Service (Client Credentials)

Go services obtain access tokens using Client Credentials at startup. The Keycloak client_secret is never in environment variables — it is injected into a local memory-mapped file by the OpenBao agent sidecar. OpenBao rotates secrets every 24 hours. Each service caches its access token with a TTL of (expires_in - 30s) and re-fetches transparently on expiry.

JWT Validation Flow

Inbound request


APISIX Gateway
  1. Extract Bearer token from Authorization header
  2. Fetch Keycloak JWKS (cached 5 min, fallback to cached copy on failure)
  3. Verify signature (RS256), issuer, audience, expiry
  4. OPA plugin: coarse-grained role + path check
  5. Forward request with X-User-ID, X-Member-ID, X-Roles headers


Go service pod
  6. auth.JWTMiddleware: re-validate token (defense in depth; JWKS cached in-process)
  7. OPA sidecar: fine-grained ABAC check (e.g., does member_id match the resource?)
  8. Handler executes
  9. PHI audit log written to OpenSearch

Internal routes (/internal/*) skip steps 1–7 at the service layer; they are accessible only within the Istio service mesh (enforced via NetworkPolicy and AuthorizationPolicy).

OPA Roles

Roles are defined in Keycloak realm roles and referenced in OPA Rego policies. A user may hold multiple roles. OPA decisions are logged to the opa-decisions OpenSearch index; denied decisions include the policy package, rule, and input.

RoleRealmPermissions Summary
MEMBERolly-membersView own claims, submit claims, view EOBs, manage own enrollment, view coverage details
EMPLOYER_ADMINolly-membersAll MEMBER permissions for their group, manage group enrollment, view group census, billing overview
PROVIDERolly-providersView claims for their patients (filtered by NPI), submit 837 EDI, view prior auth and credentialing status
PROVIDER_BILLING_ADMINolly-providersAll PROVIDER permissions plus payment/835 remittance history and billing contact management
CLAIMS_ADJUSTERolly-internalRead all claims, adjudicate, approve/deny prior auths, view member PHI (audit-logged), generate EOBs
UNDERWRITERolly-internalView actuarial data, rate tables, enrollment analytics, read-only claims aggregates
NETWORK_MANAGERolly-internalManage provider credentialing workflows, approve/deny network contracts, update provider directory
SYSTEM_ADMINolly-internalAll permissions, Keycloak realm administration, OPA policy deployment, OpenBao secret management

Principle of least privilege: CLAIMS_ADJUSTER cannot modify enrollment records. EMPLOYER_ADMIN cannot view claim detail beyond their own group. PROVIDER can only see claims where their NPI appears as the rendering or billing provider.

Session TTLs

ParameterMobileWeb (Member / Provider)Web (Admin / Internal)
Access token TTL15 min15 min15 min
Refresh token TTL24 h8 h4 h
Max session duration30 days (with activity)8 h (hard)4 h (hard)
Idle timeout30 min30 min15 min
Token storageExpo SecureStorehttpOnly cookiehttpOnly cookie
Refresh token rotationYesYesYes

Refresh token rotation: Every use of a refresh token issues a new refresh token and invalidates the old one. If a previously-used (stolen) refresh token is presented, Keycloak invalidates the entire session family. Forced re-authentication is triggered on: password change, account compromise action, 30-day hard session expiry, and high-sensitivity endpoints (e.g., SSN viewing) that require max_age=0.

PHI Audit Logging

Every API response that includes PHI fields is audit-logged to satisfy HIPAA §164.312(b) (Audit Controls). Each log entry records:

FieldSource
timestampRequest timestamp (UTC)
request_idUnique ID generated by APISIX, propagated via X-Request-ID
actor_idJWT sub claim (Keycloak user UUID)
actor_typeMEMBER, PROVIDER, INTERNAL, or SERVICE
actor_rolesJWT realm_access.roles
client_ipOriginal client IP from X-Forwarded-For (set by APISIX)
endpointURL path (no query params to avoid leaking filter values)
resource_typeCLAIM, MEMBER, EOB, PRIOR_AUTH, etc.
resource_idUUID of the accessed resource
member_idMember whose PHI was accessed (may differ from actor)
phi_fields_returnedArray of PHI field names in the response
opa_decisionALLOW or DENY and the matched policy rule

Log destinations:

  1. AWS CloudTrail — API call-level logging for AWS SDK calls (S3 object access, KMS decrypt). Stored in S3 with S3 Object Lock (WORM mode), 6-year retention.
  2. OpenSearch audit-logs index — Application-level PHI access logs. Indexed by member_id, actor_id, timestamp, resource_type. 6-year retention via ILM policy. Encrypted at rest with KMS CMK.
  3. Grafana Loki — Operational log stream. 90-day retention (operational use only, not compliance).

Access to the audit-logs index is restricted to SYSTEM_ADMIN role and automated log-writing service accounts.

Olly Health Insurance Platform