ConnectSecurity

Security model

This page documents what Loop’s identity service enforces and what your app must do to remain compliant.

PKCE is required for everyone

Confidential clients (web apps with a secret) and public clients (mobile, SPAs, CLIs) both must use PKCE. The code_challenge_method=plain option is rejected; only S256 is accepted.

Reason: PKCE is cheap and removes an entire category of code-interception attacks. There’s no upside to skipping it.

Redirect URIs

  • Must be an exact match of one of the URIs registered for your app. No wildcards, no path patterns.
  • Must be HTTPS in production. http://localhost:* is allowed only for development; we recognize it by hostname.
  • Must be a valid URL, no fragments, no query strings.
  • Mobile apps using custom schemes (com.example.app://callback) are supported. The scheme must be unique to the app and registered.

If your redirect URI doesn’t match exactly, identity refuses to redirect — the user sees an error page and your app gets nothing.

State and nonce

  • state is required on every /authorize call. It’s how your app detects CSRF.
  • nonce is required if you ask for openid scope (i.e., you want an id_token). It binds the id_token to your specific authorization request.

Generate both with a CSPRNG. Treat them as opaque, single-use values.

Client secrets

  • Web apps (server-side) store the secret server-side. Never embed it in client-side JS.
  • Mobile / SPA / native apps have no secret. They are public clients; they rely on PKCE and don’t authenticate to /token.
  • Rotation: you can rotate a client secret from the developer portal. The old secret continues to work for 24 hours after rotation, then dies.
  • Leak handling: if you suspect a leak, rotate immediately. Loop will also rotate it if our monitoring sees an anomalous pattern from your client_id.

CORS

  • /v1/oauth/authorize is browser-navigated (top-level), so CORS doesn’t apply.
  • /v1/oauth/token accepts requests from any origin (you call it from your backend, not the browser).
  • Service endpoints (<service>.platform.loop.health/v1/*) reject browser fetches with Origin headers unless the origin matches an entry in your app’s allowed_origins list (configured at registration).

If you want a SPA to call services directly from the browser, register the origin. Don’t try to bypass the check by manipulating headers — services validate via the network edge.

What your app must never do

  • Store the user’s password. Loop never gives it to you. You never collect it.
  • Reuse refresh tokens. Each refresh rotates. Reuse triggers a security alert and revokes the grant.
  • Log access tokens. They identify a user and grant data access. Treat like passwords. Redact from any logs.
  • Pass tokens through URLs. Tokens go in Authorization headers or POST bodies. Never query params.
  • Cache user data longer than the user agreed to. Your data-retention policy should be documented and enforced.
  • Make calls outside granted scopes. You’ll get 403, but trying it repeatedly is a signal Loop’s anomaly detection will catch.

HIPAA implications

Loop is a HIPAA-adjacent platform. Some scopes return PHI (Protected Health Information). Apps that request those scopes:

  • Must have a BAA (Business Associate Agreement) with Loop. Apply via developers.platform.loop.health/baa.
  • Cannot store PHI on unencrypted devices or transmit it over unencrypted channels.
  • Must support a user’s right to revoke and a Loop-initiated revocation if the BAA is terminated.

Scopes that return PHI are marked in scopes.md. The consent screen surfaces this to users with stronger language (“This app will see your detailed health information”).

Apps that don’t have a BAA and request a PHI scope: the authorization request fails with error=invalid_scope and a remediation link.

Audit log

Every authorization, token issuance, refresh, revocation, and API call carrying an OAuth token is logged in identity.audit_log (and forwarded to the per-service audit log on the service that handled the call). Users can request their own audit log via GET /v1/users/me/audit-log (with scope read:account).

Rate limits

EndpointLimitPer
/v1/oauth/authorize30 req/minper IP
/v1/oauth/token60 req/minper client_id
/v1/oauth/introspect600 req/minper client_id
/v1/oauth/revoke60 req/minper client_id

429s are returned with Retry-After. Exponential back-off is required; repeat 429 violation triggers a temporary client_id lockout.

Reporting a vulnerability

security@loop.health. PGP key published at https://platform.loop.health/.well-known/security.txt. We respond within one business day; in-scope reports under 90 days get a bounty.