Nincs express-session, nincs passport, és a memóriában tartott session-store sem működik. Ez a modul végigveszi, hogyan lesz mégis bejelentkezés, jogosultság és tenant-azonosítás egy Workers-alapú multitenant SaaS-ban — a session tárolásától az enterprise SSO-ig.
A Node-világban a session-kezelés jellemzően így nézett ki: a szerver memóriájában (vagy Redisben) van egy session-store, a kliens kap egy session-ID-t cookie-ban, és minden kéréskor kikeresed. Workersön ebből két dolog törik el:
Ebből következik a modul központi döntése: hol él a session állapota?
| Stratégia | Hogyan | Előny | Hátrány |
|---|---|---|---|
| 1. Aláírt/titkosított cookie (stateless) |
a session-adat maga a cookie-ban van, kriptográfiailag lezárva (sealed); a szerver csak felnyitja | nulla lekérdezés, tökéletesen edge-barát, nincs mit skálázni | méretkorlát (~4 KB), és nem lehet azonnal visszavonni (a kiadott cookie a lejáratáig él) |
| 2. KV-alapú session | cookie-ban csak az ID; az adat KV-ben, globálisan replikálva | gyors olvasás mindenhonnan, nagy adat is elfér, kilistázható | eventually consistent (6. modul): a visszavonás ~60 s-ig „átcsúszhat" |
| 3. DB-alapú session (D1/Postgres) | klasszikus session-tábla | azonnali visszavonás, auditálható, tranzakcionális | minden kérés egy DB-kör (a primary régióba — 5b. modul!) |
| 4. Durable Object per user | a session-állapot egy címezhető DO-ban | erős konzisztencia, azonnali kiléptetés, élő kapcsolatkezelés | komplexebb; a DO helye számít (7. modul) |
Nuxtban a nuxt-auth-utils pontosan ezt a mintát adja: lezárt (sealed) cookie-session, ami a Workers-runtime-ban is működik, mert csak Web Crypto kell hozzá.
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event);
const user = await verifyCredentials(event, email, password); // lásd 17.4
if (!user) throw createError({ statusCode: 401 });
await setUserSession(event, {
user: { id: user.id, email: user.email },
tenant: { id: user.tenant_id, role: user.role }, // a tenant a session RÉSZE
loggedInAt: new Date().toISOString(),
});
return { ok: true };
});
A titok a NUXT_SESSION_PASSWORD (min. 32 karakter), ami secretként él (8. és 12. modul): .dev.vars lokálisan, wrangler secret put élesben.
Itt van egy fontos runtime-korlát, amivel Node-ból érkezve szembe fogsz futni. A bcrypt natív bináris — nem fut isolate-ben (2. modul). Az argon2 ugyanígy. Ami működik:
| Megoldás | Állapot | Megjegyzés |
|---|---|---|
| PBKDF2 a Web Crypto API-val | natívan, gyorsan | a platform beépített, hardveresen gyorsított implementációja; magas iterációszámmal ma is elfogadott |
| scrypt / bcrypt WASM-portban | működik | de figyelj a CPU-limitre (2. modul): a szándékosan lassú hash pont a CPU-t eszi |
| bcrypt / argon2 natív | nem | natív bináris — Containers kellene hozzá |
| Külső auth-szolgáltató | megkerülöd | ha nem akarsz jelszót kezelni: OAuth/OIDC szolgáltató (lásd 17.6) |
const ITER = 210_000; // igazítsd a CPU-budgetedhez, mérd meg! (9. modul)
export async function hashPassword(password: string) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits"]);
const bits = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt, iterations: ITER, hash: "SHA-256" }, key, 256);
return `pbkdf2$${ITER}$${b64(salt)}$${b64(new Uint8Array(bits))}`;
}
export async function verifyPassword(stored: string, password: string) {
const [, iter, salt, hash] = stored.split("$");
// … ugyanaz a derive, majd IDŐÁLLANDÓ összehasonlítás:
return timingSafeEqual(computed, hash);
}
Multitenant SaaS-ban az auth nem ér véget a „ki vagy?" kérdésnél — a lényeg a „melyik tenant nevében, milyen szerepkörrel?". Három forrásból jöhet a tenant, és ezt tudatosan kell eldönteni:
| Forrás | Mikor | Kockázat |
|---|---|---|
Aldomain / custom domainacme.app.com | ha minden tenant saját hostot kap (16. modul: Cloudflare for SaaS) | alacsony — a host nem hamisítható a session mellett |
| Session (a bejelentkezéskor rögzített tenant) | ha egy user egy tenanthoz tartozik | alacsony — szerveroldalról jön |
| Kérés-paraméter (path, header) | ha egy user több tenantban is tag | magas — mindig ellenőrizni kell a tagságot |
A helyes minta: a tenant-feloldás egyetlen middleware-ben történjen, egyszer, és onnantól a kontextusból jöjjön — soha ne minden endpoint oldja meg újra (13. modul elve):
server/middleware/01.tenant.tsexport default defineEventHandler(async (event) => {
if (!getRequestURL(event).pathname.startsWith("/api/")) return;
const session = await getUserSession(event);
if (!session?.user) return; // publikus végpont
// 1) a hostból megpróbáljuk (custom domain / aldomain)
const hostTenant = await tenantFromHost(event);
// 2) ha van host-tenant, ellenőrizzük a TAGSÁGOT — ez a kulcslépés
const tenantId = hostTenant?.id ?? session.tenant.id;
if (hostTenant && hostTenant.id !== session.tenant.id) {
const member = await useDb(event).prepare(
"SELECT role FROM memberships WHERE user_id = ? AND tenant_id = ?"
).bind(session.user.id, hostTenant.id).first();
if (!member) throw createError({ statusCode: 403 });
event.context.role = member.role;
} else {
event.context.role = session.tenant.role;
}
event.context.tenantId = tenantId; // innentől EZT használja mindenki
event.context.userId = session.user.id;
});
tenantId-t, vagy a lekérdezésből kimarad a tenant-feltétel. Két védelem: ① a tenant soha ne a kliens kéréséből jöjjön szűrés nélkül; ② az adathozzáférés menjen egy vékony rétegen át, ami mindig beteszi a tenant-szűrést. A 13. modul server/utils mintája erre való: ha a useDb() helyett egy tenantQuery(event, sql, params) segédet használsz, ami automatikusan hozzáfűzi a tenant_id = ? feltételt, akkor a hibalehetőség egyetlen fájlba szorul. Írj rá tesztet is (11. modul): „A tenant usere nem éri el B tenant rekordját" — ez az egyik legfontosabb tesztesetetek.| Megoldás | Modell | Workers-alkalmasság |
|---|---|---|
| nuxt-auth-utils | lezárt cookie-session, minimalista; a szolgáltatói OAuth-folyamatokat is adja | jó — Web Crypto alapú, kevés függőség |
| better-auth | teljes auth-keretrendszer (email/jelszó, OAuth, 2FA, szervezetek/tagságok, session-kezelés), adapterekkel | jó — D1/KV-vel bevált; Nuxt-modul is van hozzá |
| Saját JWT/cookie | a 17.2–17.3 minták kézzel | teljes kontroll, de te felelsz a kriptó-részletekért |
| Passport / express-session | Node-középpontú middleware-lánc | nem — Express-alapú, Node API-kra épül |
| Külső szolgáltató (Auth0, Clerk, WorkOS, Keycloak…) | OIDC/OAuth kívülről | működik — a Worker csak tokent validál (JWKS) |
Egy B2B SaaS életében biztosan eljön a pillanat, amikor egy nagy ügyfél azt mondja: „a mi felhasználóink az Okta/Entra ID/Google Workspace fiókjukkal lépjenek be". Négy út van:
Amit az architektúrádban előre készíts elő, még mielőtt az első ügyfél kéri: a tenants táblában legyen hely az SSO-konfignak (sso_type, issuer, client_id, domain), és a login-folyamat tudjon email-domain alapján irányítani: ha valaki @acme.com címmel próbál belépni, és az acme tenantnál SSO van bekapcsolva, a jelszavas belépés helyett az ő identity providerükhöz kell átdobni. Ez a „home realm discovery", és utólag beépíteni sokkal fájdalmasabb.
crypto.getRandomValues), megmutatod egyszer, és a DB-be csak a hash kerül; ellenőrzéskor a beérkező kulcsot hasheled és úgy keresed. Legyen prefixe (sk_live_…), hogy felismerhető legyen a logokban és a titok-szkennerekben, és tárold hozzá a tenantot, a jogosultságokat és a lejáratot.HttpOnly, Secure, SameSite=Lax (vagy Strict), reális lejárattal./api/auth/login-ra (9. modul) és fiókonkénti számláló (DO).Nincs megbízható processz-memória (az isolate eldobható, és sok példányban fut), a központi store pedig elveszi az edge latencia-előnyét. Ajánlott: rövid lejáratú aláírt cookie-session (nulla lekérdezés) + hosszú életű, visszavonható refresh-token a DB-ben; azonnali globális kiléptetéshez revocation epoch.
A natív bcrypt nem fut isolate-ben. A régi hasheket WASM-bcrypttel ellenőrzöd belépéskor, és sikeres belépéskor újrahasheled PBKDF2-vel (Web Crypto). A maradék felhasználót idővel jelszó-visszaállításra kéred.
Egyetlen middleware-ben, a hostból és/vagy a session-ből, tagság-ellenőrzéssel; onnantól a kontextusból. A tipikus hiba (IDOR), hogy az endpoint elhiszi a kérésben érkező tenant-azonosítót, vagy kimarad a tenant-szűrés a lekérdezésből — ezért kell egy központi adathozzáférési réteg, ami mindig hozzáteszi.
Lehetőségek: saját OIDC (ha az ügyfél tudja), SSO-bróker szolgáltató a SAML-hez, vagy teljes auth-platform. Előre építsd be: SSO-konfigmezők a tenants táblába, és email-domain alapú irányítás a login-folyamatba (home realm discovery) — utólag ez fájdalmas.
Csak a hashüket a DB-ben (a nyers kulcsot egyszer mutatod meg), felismerhető prefixszel, a tenanthoz és jogosultságokhoz kötve, lejárattal; ellenőrzéskor a beérkező kulcsot hasheled. Belső Worker→Worker híváshoz viszont ne kulcs, hanem service binding.