Skip to content

Console authentication

OwlEye has two deliberately separate browser surfaces:

  • Visitor analytics uses the public SDK and does not write cookies, local storage, or session storage.
  • The private console uses secure authentication cookies because it contains account and app controls.

An authenticated console does not make the tracking SDK less cookie-free. They are different data paths with different jobs.

Email sign-in is passwordless:

  1. POST /v1/auth/email/start accepts an email address and sends a six-digit one-time code.
  2. POST /v1/auth/email/verify consumes that code once.
  3. Accounts with 2FA enabled complete POST /v1/auth/2fa/verify with a current authenticator code.

Public signup rejects disposable-email domains and any address whose local part contains + (for example, name+project@gmail.com). The same eligibility check runs before both email-OTP and Google account creation, and uses one stable error message. This is an initial anti-abuse boundary, not proof that an accepted mailbox belongs to a trustworthy human; rate limits and provider-side abuse monitoring still matter.

Google sign-in starts at GET /v1/auth/google/start. The API uses a one-time OAuth state, PKCE S256, and a short-lived browser-binding cookie. Only a hash of that HttpOnly, SameSite=Lax binding is stored; the callback must present it before the state can be consumed, and the cookie is cleared on callback success or failure. This prevents a login initiated in one browser from being swapped into another. The API exchanges the authorization code server-side, then returns to the console. Configure the canonical callback as /v1/auth/google/callback; legacy route aliases remain for compatibility.

Local OWLEYE_EMAIL_MODE=log writes OTP codes to the API log for development. Use SMTP in any real deployment. Never use log mode on a public server.

Successful sign-in sets two HttpOnly, SameSite=Lax cookies. JavaScript cannot read either value, and OwlEye never copies them into local or session storage.

  • The access credential lasts 60 minutes by default.
  • The opaque refresh credential lasts 30 days by default and is scoped to /v1/auth.
  • POST /v1/auth/refresh rotates both credentials and slides the refresh expiry another 30 days.
  • A replay of the immediately previous refresh credential revokes that session family.
  • The previous access credential has a 30-second server-side grace window after rotation so an already-running API request can finish. Refresh credential replay detection remains strict.
  • Logout and logout-all revoke SQLite session records and clear both cookies.

The session endpoint returns the actual access expiry. The console schedules rotation from that value with a TTL-adaptive safety margin while the page is visible, so custom access lifetimes from 5 minutes through 24 hours work without a hard-coded timer. Tabs coordinate through Web Locks when available and a short-lived local-storage lease otherwise. Local storage contains only lease and expiry timestamps—never access or refresh credentials. Session bootstrap performs one refresh and retry when an access credential has expired. Other feature requests are not automatically replayed after a 401, which avoids duplicating writes; a future shared API client may add one bounded retry for explicitly safe reads. Refresh values are hashed in SQLite; TOTP seeds are encrypted at rest with a versioned key derived from OWLEYE_HASH_SALT. Existing plaintext TOTP rows remain readable and are upgraded after a successful 2FA login.

Production HTTPS deployments must use secure cookies. OwlEye enables them automatically when OWLEYE_APP_URL starts with https://; set OWLEYE_COOKIE_SECURE=true explicitly when a proxy or unusual deployment obscures that scheme. Hosted production fails startup when secure cookies or SMTP email delivery are disabled.

The first successful email or Google sign-in opens a skippable authenticator-app setup. OwlEye supports standard TOTP authenticator apps here—no SMS or email MFA substitute. Completing or skipping the prompt is stored on the user in SQLite, so it does not reappear on every device.

Next, the owner creates an app with:

  • a required app name;
  • an optional comma-separated list of allowed domains.

All loopback origins (localhost, *.localhost, 127.0.0.0/8, and ::1) remain available for local development. Specified domains are normalized and inserted with the app in one transaction. The console then reveals the API-created tracking ID, public key, and SDK snippet.

Terminal window
OWLEYE_APP_URL=https://app.example.com
OWLEYE_ALLOWED_ORIGINS=https://app.example.com
OWLEYE_HASH_SALT=<at-least-32-random-bytes>
OWLEYE_SESSION_COOKIE=owleye_session
OWLEYE_REFRESH_COOKIE=owleye_refresh
OWLEYE_OAUTH_BINDING_COOKIE=owleye_oauth_binding
OWLEYE_ACCESS_TOKEN_MINUTES=60
OWLEYE_REFRESH_TOKEN_DAYS=30
OWLEYE_COOKIE_SECURE=true
OWLEYE_EMAIL_MODE=smtp
OWLEYE_SMTP_HOST=smtp.example.com
OWLEYE_SMTP_PORT=587
OWLEYE_SMTP_USERNAME=<smtp-user>
OWLEYE_SMTP_PASSWORD=<smtp-password>
OWLEYE_SMTP_FROM=analytics@example.com
OWLEYE_GOOGLE_CLIENT_ID=<google-client-id>
OWLEYE_GOOGLE_CLIENT_SECRET=<google-client-secret>
OWLEYE_GOOGLE_REDIRECT_URL=https://api.example.com/v1/auth/google/callback

Keep OWLEYE_HASH_SALT stable and secret. Changing it invalidates access credentials and prevents existing encrypted TOTP seeds from being opened.

GET /v1/account/export downloads the signed-in user’s account and control-plane data as JSON. DELETE /v1/account deletes the live canonical sign-in account after the user transfers or deletes every app and organization they own. Both routes reject read-only preview workspaces; deletion also clears the session cookies. Audit evidence, external recipients, logs, and backups need separate reviewed retention, while analytics facts remain under the app-owner lifecycle so an account cannot silently orphan or erase another member’s app.