Skip to content

Authentication

All API requests to OpenInsure must be authenticated using a Bearer token in the Authorization header.

OpenInsure uses a dedicated Auth Worker (oi-sys-auth) running on Cloudflare Workers with Cloudflare D1 (edge SQLite) as the identity store. User accounts, sessions, and organization memberships are managed in the D1 database oi-auth:

  • user — User accounts (id, email, role, orgId)
  • session — Active sessions
  • account — OAuth/provider accounts (Microsoft SSO)
  • verification — Email/phone verifications

The auth service is deployed at https://auth-dev.openinsure.dev.

Terminal window
POST https://auth-dev.openinsure.dev/api/auth/sign-in/email
Content-Type: application/json
{
"email": "user@example.com",
"password": "••••••••"
}

Response:

{
"user": { "id": "usr_…", "email": "user@example.com", "role": "org_admin" },
"session": { "id": "ses_…", "expiresAt": "2026-03-08T14:00:00Z" },
"token": "eyJhbGciOiJIUzI1NiJ9…"
}

The response also sets an oi_session cookie that portal middleware reads automatically.

Microsoft sign-in is restricted to the mhcis.com Azure AD tenant (AzureADMyOrg). The app registration “OpenInsure Auth” is a single-tenant registration managed in OpenTofu at infra/azure-auth/main.tf.

Tenant ID: ebd58a52-c818-4230-b150-348ae1e17975

Redirect URIs:

EnvironmentURI
Productionhttps://auth-dev.openinsure.dev/api/auth/callback/microsoft
Local devhttp://localhost:8788/api/auth/callback/microsoft

Flow:

  1. Redirect the user to the auth worker:

    GET https://auth-dev.openinsure.dev/api/auth/sign-in/microsoft?callbackURL=<portal_url>/api/auth/callback
  2. Auth worker redirects to Azure AD for user authentication.

  3. Azure AD redirects back to /api/auth/callback/microsoft on the auth worker.

  4. Auth worker issues a signed JWT and redirects to <portal_url>/api/auth/callback?token=<JWT>.

  5. Each portal’s /api/auth/callback route reads ?token=JWT, sets the portal-specific cookie, and redirects to the dashboard.

EndpointMethodDescription
/api/auth/get-sessionGETReturns { user, session, token } from the oi_session cookie
/api/auth/sign-outPOSTInvalidates the current session and clears the cookie
/api/auth/sign-up/emailPOSTCreates a new user account with email + password

All OpenInsure JWTs are HS256 signed with an 8-hour TTL.

{
"sub": "usr_01J8K3M4N5P6Q7R8S9T0",
"org": "org_01HW3K2T9Z6D4V8X5N1M",
"role": "org_admin",
"iat": 1741430400,
"exp": 1741459200
}

Each portal verifies its own JWT cookie using a portal-specific secret:

PortalCookie nameEnv secretWorker
Admin (MHC IS ops)oi_admin_tokenJWT_SECREToi-sys-admin
Financeoi_finance_tokenFINANCE_JWT_SECREToi-sys-finance
Complianceoi_compliance_tokenCOMPLIANCE_JWT_SECREToi-sys-compliance
Carrier portaloi_carrier_tokenCARRIER_JWT_SECREToi-sys-carrier
Producer portaloi_producer_tokenPORTAL_JWT_SECREToi-sys-producer
Underwriting Workbenchoi_uw_tokenJWT_SECREToi-sys-uw
Policyholder portaloi_sessionPOLICYHOLDER_JWT_SECREToi-sys-portal
Mobile appSecureStore (Keychain)POLICYHOLDER_JWT_SECRETDirect API call

Every portal has an /api/auth/callback route that:

  1. Reads the ?token=JWT query parameter.
  2. Verifies the JWT against JWT_SECRET.
  3. Sets the portal-specific cookie.
  4. Redirects to the dashboard.

Policyholders authenticate via a 2-step OTP flow — no passwords are stored.

Terminal window
# Step 1: Request OTP (sends 6-digit code to email on file)
POST /auth/policyholder-otp-request
Content-Type: application/json
{ "policyNumber": "MHC-COM-2026-04821" }
Terminal window
# Step 2: Verify OTP and receive JWT
POST /auth/policyholder-token
Content-Type: application/json
{ "policyNumber": "MHC-COM-2026-04821", "otp": "123456" }

Response:

{
"token": "eyJhbGciOiJIUzI1NiJ9…",
"role": "policyholder",
"orgId": "org_uuid",
"sub": "policy-id",
"policyNumber": "MHC-COM-2026-04821",
"insuredName": "Midwest Freight Corp"
}
  • OTP is stored in KV with 10-minute TTL and cleared after use
  • JWT includes sub (policy ID), org, role, policyNumber, insuredName
  • The mobile app stores the JWT in SecureStore (Keychain) and adds biometric re-auth
  • The web portal sets it as the oi_session cookie
import { AuthProvider, useAuth } from '@openinsure/auth-hooks';
function App() {
return (
<AuthProvider baseURL="/api/auth">
<Dashboard />
</AuthProvider>
);
}
function Dashboard() {
const { user, token, loading, login, logout } = useAuth();
if (loading) return <div>Loading...</div>;
if (!user) return <div>Please log in</div>;
return (
<div>
<p>Welcome, {user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}

useAuth() is provided by @openinsure/auth-hooks and works in all Next.js portals as well as apps/workbench (re-exported via context/AuthContext.tsx).

API keys are the primary way to authenticate server-to-server requests. You can generate and manage API keys in the Admin Dashboard.

PrefixEnvironmentUsage
oik_live_ProductionLive production data
oik_test_SandboxTesting and development
Terminal window
curl -X GET https://api.openinsure.dev/v1/policies \
-H "Authorization: Bearer oik_live_abc123..."

OpenInsure uses Role-Based Access Control (RBAC) with the following common roles:

RolePortalAccess
superadminAllFull system access across all organizations
org_adminAdmin / CarrierFull access within their organization
mhcis_operatorAdmin (oi-sys-admin)Platform vendor ops: orgs, programs, rate tables, rules, API keys, audit. Cross-org access via SpiceDB mhcis relation.
finance_analystFinance (oi-sys-finance)TigerBeetle ledger views, AR aging, statutory reports, reconciliation, report builder
compliance_officerCompliance (oi-sys-compliance)Producer licensing, state filings, compliance analytics, audit log. No PHI values.
underwriterUW WorkbenchAccess to submissions, policies, and rating
producerProducer portalAccess to their own submissions and policies
claims_adjusterAccess to assigned claims and related policy data
claims_supervisorFull PHI access for all org claims; approves settlements
billing_adminAccess to invoices, payments, and financial reports
auditorRead-only access with mandatory audit logging
policyholderPolicyholder portalPortal-only JWT; read-only access to own policy and claims

For complex permission scenarios, OpenInsure integrates with SpiceDB (Zanzibar-style permissions). This allows fine-grained access control like:

  • “Can this Producer view a Claim linked to their Policy?”
  • “Can this Underwriter approve a submission above their authority limit?”

Every API key and JWT is scoped to a specific Organization ID (org_id). The platform enforces strict isolation at the application layer using:

  1. JWT Claims — Every token contains an org claim
  2. App-Level Tenant Isolation — All queries are scoped by orgId in application middleware, ensuring queries only return rows matching the user’s organization
  3. Application Middleware — All API routes validate org scope before processing
// Example: orgId scoping applied by middleware before every query
const policies = await db.select().from(policiesTable).where(eq(policiesTable.orgId, ctx.orgId));

Frontend applications should set the following environment variables:

VariableDescription
VITE_AUTH_URLAuth API base URL (e.g., https://auth-dev.openinsure.dev)
NEXT_PUBLIC_AUTH_URLFor Next.js apps

Backend/API environment variables:

VariableDescription
AUTH_URLAuth worker base URL (e.g., https://auth-dev.openinsure.dev)
AUTH_SECRETAuth worker signing secret — set via wrangler secret put
JWT_SECRETAdmin + UW Workbench JWT signing key — set via wrangler secret put
FINANCE_JWT_SECRETFinance portal JWT signing key — wrangler secret put FINANCE_JWT_SECRET --name oi-sys-finance
COMPLIANCE_JWT_SECRETCompliance portal JWT signing key — wrangler secret put COMPLIANCE_JWT_SECRET --name oi-sys-compliance
CARRIER_JWT_SECRETCarrier portal JWT signing key
POLICYHOLDER_JWT_SECRETPolicyholder portal JWT signing key
PORTAL_JWT_SECRETProducer portal JWT signing key
API_SECRETMachine/system bearer token — set via wrangler secret put