Skip to main content

Security model

IdentSphere's security model rests on five principles. If you understand these, the rest of the security choices flow naturally.

1. Zero phone-home

The SDK never opens a network connection to any server controlled by the IdentSphere maintainers — at any tier. No license-check ping. No usage analytics. No install ping. No CVE alerts pushed from us to you. Nothing.

This means:

  • Air-gapped deployments work out of the box
  • Compliance reviews (HIPAA, SOC2, EU GDPR) cannot find any third-party data flow to flag
  • The IdentSphere project could disappear tomorrow and your auth keeps running indefinitely

License verification (for the paid premium modules, when those ship) is offline-only via ed25519-signed JWTs verified against an embedded public key.

2. You own all the data

Every byte of user data lives in your Postgres. IdentSphere never persists data anywhere we can see. We don't have your users' emails, password hashes, MFA secrets, OAuth tokens, audit logs, or session data — by design, with no exception.

This is true at every license tier.

3. Standard primitives only

We use:

ConcernAlgorithmWhy
Password hashingArgon2id (preferred) + bcrypt (legacy)NIST + OWASP recommended; every other auth provider can import these hashes
Token signingRS256 (production) + HS256 (dev)Asymmetric, JWKS-publishable; every JWT library validates these
MFATOTP (RFC 6238)Compatible with every authenticator app
PasskeysW3C WebAuthn level 2Industry standard, hardware-backed
OAuthOAuth 2.1 + OIDCRFC 6749 + 8252 compliant
Cookie bindingDPoP (RFC 9449)Tokens cryptographically bound to client keypair
Webhook signingHMAC-SHA256 with timestampSame as Stripe / GitHub patterns

No proprietary anything. Every artifact IdentSphere issues can be validated by a third-party library in any language. This is the anti-lock-in promise made physical.

4. Fail closed

Authorization decisions default to "no" when the data needed to make the decision is missing.

Examples:

  • Authorizer::resolve_role returns Err(Forbidden) if no organization_memberships row exists. There's no "default member" fallback that could let a removed user keep their old permissions.
  • validate_jwt checks the user_sessions table; a session with no row is treated as revoked (not as "unknown therefore allow").
  • require_recent_mfa defaults to requiring MFA when the timestamp is ambiguous or unset.
  • Platform admin lookup defaults to (false, None) on a database error — a transient outage cannot accidentally grant admin powers.

5. Defense in depth on cookies

Browser sessions are protected by three independent mechanisms:

CookieWhat it carriesDefense
identsphere_atAccess token (JWT)HttpOnly — JS can't read it. XSS can't steal it.
identsphere_rtRefresh tokenHttpOnly + Path=/v1/auth — JS can't read AND only sent to refresh endpoint
identsphere_csrfCSRF tokenNOT httpOnly — JS reads and echoes in X-IdentSphere-CSRF header on mutating requests

Production cookies also get Secure + SameSite=Lax.

This means an attacker would need:

  • XSS access to your frontend AND the ability to forge a same-origin POST AND knowledge of the user's refresh path — AND the user must trigger that POST before the access token expires (default 15 min).

What IdentSphere does not protect

These are the customer's responsibility — IdentSphere gives you the rope but doesn't tie the knot:

  • TLS termination. Run a real TLS reverse proxy (nginx, Caddy, Cloudflare, AWS ALB). IdentSphere has no opinions about how you get there.
  • Rate limiting. IdentSphere ships HMAC anti-brute on MFA challenges but not on the overall request rate. Wire tower-governor, Cloudflare, or nginx-rate-limit in front. Recommended starting points are in the rate limiting docs.
  • Secret management. IDENTSPHERE_JWT_SECRET and IDENTSPHERE_RSA_PRIVATE_KEY_PEM must come from a real secret manager (AWS Secrets Manager, Vault, Doppler, 1Password CLI). Don't put them in .env files in production.
  • Network segmentation. Run IdentSphere behind a private network if at all possible. Public Postgres is never a good idea.

See also