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 * 30, // sign staff out after 30 idle minutes
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 hour of inactivity | 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 - a password or a user-verified passkey, checked against live staff access. Neither a public session nor social SSO ever becomes an AdminCP session.
AdminCP Session Timeout
The AdminCP is the keys to the kingdom, so its session is deliberately short-lived. Staff are asked to sign in again when:
- They go quiet for an hour. Every AdminCP request pushes the expiry an hour ahead, so an admin who keeps working is never interrupted. Leave the tab alone for an hour and it signs itself out and lands on the sign-in page, with a toast explaining why. Tune the window with
adminCookieExpires. - They close every AdminCP tab. Open AdminCP tabs keep a heartbeat going. Open the AdminCP again after every tab was closed and VitNode ends the old session instead of letting it back in. A page reload, or opening one more AdminCP tab while another is still open, is not affected. This check is skipped when
cookieDomainis set: a browser can only see tabs on its own origin, so an AdminCP tab open on another subdomain would look closed. - They close the browser. The
vitnode_auth_admincookie has no expiry date, so the browser drops it when it shuts down.
Leaving the AdminCP counts as closing it
Moving from the AdminCP to the public site in your only AdminCP tab stops the heartbeat. Come back within about 15 seconds and you are still signed in; any later and you sign in again. Open the public site in a new tab to keep your AdminCP session.
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
Turning off password sign-in
Running a passkeys-only or social-login-only community? Switch passwords off:
export const vitNodeApiConfig = buildApiConfig({
authorization: {
password: false,
},
})With passwords off:
/loginhides the email + password form and Forgot password?. It keeps SSO buttons and Sign in with a passkey - if neither is configured, members see a "Signing in is unavailable" notice./registerhides the sign-up form. New accounts can still come in through SSO; with no SSO provider, the page says registration is unavailable and the "Sign up" link on/logingoes away./login/reset-passwordanswers "not found".- The API refuses
POST /users/sign_in(for members),/users/sign_up,/users/reset-password,/users/change-passwordand password-based SSO linking with403. - A stored password no longer counts as a way into the account, so members can't remove their last passkey unless they have another passkey or a linked SSO provider.
The AdminCP sign-in at /admin keeps its email + password form, so switching member passwords off never locks your staff out. With passkeys enabled, staff can also use AdminCP passkey sign-in. Signing out works exactly as before.
Password hashes already in the database are left alone, so switching enabled back to true restores password sign-in for everyone who had one.