Advanced

Authorization & Sessions

How VitNode manages public and admin sessions, HttpOnly cookies, device fingerprinting, and session expiration.

VitNode implements two isolated sessions: a public session for the community site and a separate admin session for the AdminCP. Both use opaque random tokens stored as SHA-256 hashes in PostgreSQL.

Quick start

Customize session parameters in apps/api/src/vitnode.api.config.ts:

apps/api/src/vitnode.api.config.ts
import { buildApiConfig } from "@vitnode/core/vitnode.config"

export const vitNodeApiConfig = buildApiConfig({
  authorization: {
    cookieExpires: 1000 * 60 * 60 * 24 * 30, // 30 days for public users
    adminCookieExpires: 1000 * 60 * 60 * 8, // 8 hours for AdminCP
    cookieDomain: ".yourdomain.com", // Optional cross-subdomain sharing
  },
})

The Two-Session Model

Session TypeCookie NameDefault LifetimeStorage TablePurpose
Public Sessionvitnode_auth90 dayscore_sessionsFrontend member authentication
Admin Sessionvitnode_auth_admin1 daycore_admin_sessionsHigh-privilege AdminCP access
Known Devicevitnode_device1 yearcore_sessions_known_devicesDevice authorization tracking

Session Isolation

Signing out of the AdminCP does not terminate the user's public session, and vice versa. An administrator compromised in a public context cannot access the AdminCP without re-authenticating with staff credentials.


Security Guarantees

  • SHA-256 Token Storage: The raw token is stored only in the user's HttpOnly cookie. The database stores only its cryptographic hash.
  • Device Pinning: Tokens are bound to a verified device ID. Tokens copied to another device without the matching device cookie are rejected.
  • Fast 60-Second Caching: Active sessions are cached in Redis for up to 60 seconds, eliminating database query overhead on repeated requests.
  • Canonical Email Identity: Addresses are stored in their canonical form, so Gmail dot, googlemail.com and +tag variants cannot become a second account. See One Mailbox, One Account.

One Mailbox, One Account

jankowalski@gmail.com and jan.kowalski@gmail.com look like two addresses. Gmail ignores the dots, so they are one mailbox - and a site that treats them as two lets the same person hold two accounts, dodge a ban, or farm their own referral link.

VitNode stores the canonical form of an address, so those tricks collapse into a single account:

Typed at sign-upStored in core_users.email
Jan.Kowalski@Gmail.comjankowalski@gmail.com
jankowalski@googlemail.comjankowalski@gmail.com
jankowalski+shop@gmail.comjankowalski@gmail.com
piotr+news@outlook.compiotr@outlook.com
zofia.nowak@company.comzofia.nowak@company.com

The rules are per-provider, because they are only true per-provider:

  • Dots are removed for gmail.com and googlemail.com only. Everywhere else a dot is part of the mailbox, so jan.kowalski@company.com is left alone.
  • googlemail.com folds into gmail.com. Google treats them as one domain.
  • +tag suffixes are dropped for Gmail and for the providers that document sub-addressing: Outlook, Hotmail, Live, MSN, iCloud, Proton, Fastmail, GMX, Yandex and Zoho. Mail sent to the canonical address still lands in the same inbox.
  • Everything else is kept as typed, lowercased. A + can be a real character in a corporate address, so guessing there would lock people out.
  • Yahoo's - aliases are deliberately left alone, because jan-kowalski@yahoo.com is a perfectly ordinary name.

Where it applies

Sign-up, sign-in, password reset, the SSO callback and AdminCP user management all resolve an address the same way. Someone who registered with a password at jankowalski@gmail.com and later clicks Sign in with Google - which hands back jan.kowalski@gmail.com - lands on their own account instead of a second one.

Upgrading an existing install

The 20260906174535_canonicalize_user_emails migration rewrites stored addresses in place. A row whose canonical form is already taken by another row is left untouched, so nobody is locked out and no two accounts are silently merged. Run this before upgrading to see whether your install has such pairs:

SELECT
  replace(split_part(split_part(lower("email"), '@', 1), '+', 1), '.', '') AS mailbox,
  array_agg("email")
FROM "core_users"
WHERE lower("email") LIKE '%@gmail.com' OR lower("email") LIKE '%@googlemail.com'
GROUP BY 1
HAVING count(*) > 1;

authorization Options Reference

Prop

Type

Learn More