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.
Sign-in flow
Section titled “Sign-in flow”Email sign-in is passwordless:
POST /v1/auth/email/startaccepts an email address and sends a six-digit one-time code.POST /v1/auth/email/verifyconsumes that code once.- Accounts with 2FA enabled complete
POST /v1/auth/2fa/verifywith a current authenticator code or an unused recovery code.
By default, public signup rejects disposable-email domains and any address whose local part contains + (for example, name+project@gmail.com); an operator can review and explicitly approve an individual address. 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 or Resend in any real deployment. Never use log mode on a public server.
Configure Google sign-in for the hosted service
Section titled “Configure Google sign-in for the hosted service”In Google Auth Platform, select your project, configure Branding and an External audience, and create an OAuth client of type Web application. Register this exact authorized redirect URI:
https://api.owleye.dev/v1/auth/google/callbackThis is a server-side redirect flow; it does not require an authorized JavaScript origin. Use the website homepage https://owleye.dev, your published privacy and terms URLs, and authorized domain owleye.dev. Choose an eligible support email from Google’s dropdown and set a monitored developer contact. Follow Google’s branding requirements to verify domain ownership and publish the app name/logo.
The code requests only openid email profile. Configure those scopes and set the audience’s publishing status to In production when ready. Google documents an exception to Testing-mode restrictions for these basic sign-in scopes; adding other scopes changes that behavior. See audience settings and Web application OAuth setup.
Set these in the private API’s server environment and restart it:
OWLEYE_GOOGLE_CLIENT_ID=<web-client-id>OWLEYE_GOOGLE_CLIENT_SECRET=<web-client-secret>OWLEYE_GOOGLE_REDIRECT_URL=https://api.owleye.dev/v1/auth/google/callbackOWLEYE_APP_URL=https://app.owleye.devOWLEYE_ALLOWED_ORIGINS=https://app.owleye.devOWLEYE_COOKIE_SECURE=trueKeep the secret on the API server. Existing hash-salt and other hosted startup requirements still apply. For local development, use a separate OAuth client with http://localhost:6174/v1/auth/google/callback and a Console URL of http://localhost:6173. The configured callback must match Google’s entry exactly.
The Console reads available sign-in methods from GET /v1/auth/config, which uses in-memory configuration without database queries; it does not depend on the strictly limited health routes. Test sign-in end to end after deployment, including an account with authenticator 2FA.
Cookie-only sessions
Section titled “Cookie-only sessions”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/refreshrotates 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 rotation through Web Locks when available. Local storage shares only refresh-completion and access-expiry timestamps, never access or refresh credentials. Automatic rotation is skipped when Web Locks are unavailable. With coordinated refresh available, session bootstrap refreshes and retries once 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 are disabled or email delivery uses log mode.
First run
Section titled “First run”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.
With no domains configured, event tracking and rule delivery accept any HTTP or HTTPS browser origin. With one or more domains configured, an origin must match one of them; matching is by hostname, so all paths on that domain are covered. Additional domains can use explicit wildcards such as *.example.com. For restricted apps, loopback origins (localhost, *.localhost, 127.0.0.0/8, and ::1) require the server’s local-development opt-in. 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.
Two-factor recovery codes
Section titled “Two-factor recovery codes”Enabling 2FA returns ten recovery codes. The Console shows them once, with copy and download controls, and asks you to confirm you saved them. Keep them in a password manager or another safe place. These are single-use codes, not a seed phrase.
If you lose your authenticator, complete your usual email or Google sign-in, choose Use a recovery code, and enter one unused code. Recovery codes replace the authenticator step; they do not replace your primary sign-in method.
Existing 2FA users can open Profile → Two-factor authentication → Generate new recovery codes. This requires a recent second-factor confirmation. Every replacement invalidates the entire previous set and requires a fresh confirmation for the next replacement. A successful 2FA sign-in qualifies for ten minutes, including when you sign in using your last recovery code.
GET /v1/auth/2fa/recovery-codes returns only the remaining count. Authenticated POST to that route generates a replacement set. The API stores only account-bound hashes in SQLite, sends recovery-code responses with Cache-Control: no-store, and atomically consumes each code with the successful action. Disabling 2FA invalidates all recovery codes.
Required configuration
Section titled “Required configuration”OWLEYE_APP_URL=https://app.example.comOWLEYE_ALLOWED_ORIGINS=https://app.example.comOWLEYE_HASH_SALT=<at-least-32-random-bytes>
OWLEYE_SESSION_COOKIE=owleye_sessionOWLEYE_REFRESH_COOKIE=owleye_refreshOWLEYE_OAUTH_BINDING_COOKIE=owleye_oauth_bindingOWLEYE_ACCESS_TOKEN_MINUTES=60OWLEYE_REFRESH_TOKEN_DAYS=30OWLEYE_COOKIE_SECURE=true
OWLEYE_EMAIL_MODE=smtpOWLEYE_SMTP_HOST=smtp.example.comOWLEYE_SMTP_PORT=587OWLEYE_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/callbackKeep OWLEYE_HASH_SALT stable and secret. Changing it invalidates access credentials and recovery codes, and prevents existing encrypted TOTP seeds from being opened.
Account data lifecycle
Section titled “Account data lifecycle”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.
Email challenge binding
Section titled “Email challenge binding”POST /v1/auth/email/start returns { "status": "sent", "challenge_id": "..." }. Keep that random challenge ID in the initiating browser flow’s memory and send it with email and code to POST /v1/auth/email/verify. An address alone cannot select and exhaust someone else’s verification attempts. The console handles this automatically.
A new code request does not invalidate an existing flow. Successful verification consumes that challenge and invalidates other outstanding login codes for the address. Expiry, atomic attempt limits, privacy-safe rate limits, and second-factor requirements still apply. Older API clients must be updated to send challenge_id; there is no unbound compatibility fallback.