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:
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 Type | Cookie Name | Default Lifetime | Storage Table | Purpose |
|---|---|---|---|---|
| Public Session | vitnode_auth | 90 days | core_sessions | Frontend member authentication |
| Admin Session | vitnode_auth_admin | 1 day | core_admin_sessions | High-privilege AdminCP access |
| Known Device | vitnode_device | 1 year | core_sessions_known_devices | Device 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
HttpOnlycookie. 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.comand+tagvariants 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-up | Stored in core_users.email |
|---|---|
Jan.Kowalski@Gmail.com | jankowalski@gmail.com |
jankowalski@googlemail.com | jankowalski@gmail.com |
jankowalski+shop@gmail.com | jankowalski@gmail.com |
piotr+news@outlook.com | piotr@outlook.com |
zofia.nowak@company.com | zofia.nowak@company.com |
The rules are per-provider, because they are only true per-provider:
- Dots are removed for
gmail.comandgooglemail.comonly. Everywhere else a dot is part of the mailbox, sojan.kowalski@company.comis left alone. googlemail.comfolds intogmail.com. Google treats them as one domain.+tagsuffixes 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, becausejan-kowalski@yahoo.comis 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