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.
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.
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.
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 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.
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.
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.
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 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.