10. modulAuth és session Nuxt-módra
Nuxt for Devs · 10. modul

Auth és session Nuxt-módra

Ez a réteg mindkét architektúrán a Nitróba kerül — a 9. modul döntésétől függetlenül meg kell csinálni, mert a cookie-kezelésnek ott van a helye, ahol a felhasználó kérése beérkezik. Végigmegyünk egy teljes bejelentkezési folyamaton, a tenant-tagság ellenőrzésén, és megnézzük, mi működik ma (2026-ban) Workers-en a jelszókezelésből — mert ez a rész az elmúlt évben érdemben változott.

10.1Miért nem ültethető át a megszokott minta

A passport + express-session + Redis hármas nem azért nem működik, mert „a Cloudflare furcsa”, hanem mert három konkrét feltételezésre épül, amiből egy sem áll:

FeltételezésMi a valóság Nitro + Workers alatt
req/res objektumok és middleware-lánc next()-telh3-ban egy event van, és a middleware visszatérési értéke a válasz (8. modul)
Hosszú életű processz, memóriában tartott session-storeIzolátumok, megosztott modul-scope — a memóriában tartott session szivárog (6. modul)
Kimenő TCP-kapcsolat RedishezWorkers-ből nincs nyers TCP tetszőleges szolgáltatáshoz. Van helyette KV, D1, Durable Object — más jellemzőkkel

A jó hír: a Nuxt-ökoszisztémában van erre kialakult, kényelmes megoldás, és a mentális modell egyszerűbb, mint a régi.

10.2A négy stratégia — és amit válassz

StratégiaHogy működikErőssége / gyengéje
Lezárt (sealed) cookie A session-adat titkosítva és aláírva magában a cookie-ban utazik. Nincs szerveroldali tároló. Nulla olvasási költség, nincs külön store.
Nem visszavonható önmagában, és mérete korlátos.
KV-store A cookie csak azonosítót tartalmaz, az adat KV-ben van. Visszavonható, nagy adat is elfér.
Végső konzisztencia (~60 mp) — a kijelentkezés nem azonnali mindenhol.
Adatbázis Session-tábla D1-ben vagy Postgresben. Azonnal konzisztens, kereshető („aktív eszközeim”).
Minden kérésnél lekérdezés.
Durable Object Felhasználónként egy objektum tartja a session-t. Erős konzisztencia, valós idejű kirúgás.
Túlzás a legtöbb SaaS-hoz.
Az ajánlott felállás: rövid életű lezárt cookie (mondjuk 15–60 perc) + egy visszavonási ellenőrzés, ami nem fut minden kérésnél. Így a normál kérések nulla extra I/O-val mennek, a kijelentkeztetés viszont mégis érvényesíthető. A 10.7-ben megnézzük, hogyan.

10.3nuxt-auth-utils — a teljes folyamat

Ez a Nuxt-világ de facto session-megoldása, és pontosan a fenti sealed-cookie mintát valósítja meg.

npx nuxi module add auth-utils
# .env — legalább 32 karakter. Élesben ez SECRET, nem env-változó!
NUXT_SESSION_PASSWORD=egy-legalabb-32-karakteres-veletlen-titok
Workers-en ezt secretként állítsd be, ne a wrangler.jsonc vars szekciójában — mert a vars tartalma látszik a dashboardon és a konfigban. npx wrangler secret put NUXT_SESSION_PASSWORD. A 12. modulban végigvesszük a runtimeConfig / env / binding / secret négyest.

Bejelentkezés

// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const { email, password } = await readValidatedBody(event, loginSchema.parse)
  const db = useDb(event)

  const user = await db.query.users.findFirst({ where: eq(users.email, email.toLowerCase()) })

  // ⚠ ugyanaz az üzenet és nagyjából ugyanaz az idő, létezik-e a felhasználó vagy sem
  if (!user || !(await verifyPassword(user.passwordHash, password))) {
    throw createError({ statusCode: 401, statusMessage: 'Hibás email vagy jelszó' })
  }

  const membership = await loadDefaultMembership(db, user.id)
  if (!membership) throw createError({ statusCode: 403, statusMessage: 'Nincs aktív munkaterület' })

  // a session TARTALMA — csak a minimum, mert minden kérésben utazik
  await setUserSession(event, {
    user: { id: user.id, name: user.name },
    tenantId: membership.tenantId,
    role:     membership.role,
    epoch:    user.sessionEpoch,      // ← a visszavonáshoz, lásd 10.7
    loggedInAt: Date.now(),
  })

  return { ok: true }
})
Mit ne tegyél a session-be. A lezárt cookie minden egyes kérésben utazik — a képekhez, a JS-fájlokhoz, mindenhez. Ha beleteszed a felhasználó teljes profilját, a jogosultsági mátrixot és a tenant beállításait, akkor minden kérést megdrágítottál, és a cookie-méret korlátjába is beleszaladhatsz. A szabály: azonosítók és néhány gyakran kellő mező. Emellett — az 5. modulból — a session tartalma az SSR alatt a payloadba is bekerülhet, tehát a felhasználó látja.

Kliensoldal

<script setup lang="ts">
const { loggedIn, user, clear, fetch: refreshSession } = useUserSession()

async function onSubmit() {
  await $fetch('/api/auth/login', { method: 'POST', body: form })
  await refreshSession()          // a kliensoldali session-állapot frissítése
  await navigateTo('/app')
}
</script>

OAuth-hoz kész handlerek vannak (defineOAuthGoogleEventHandler, defineOAuthGitHubEventHandler és több tucat másik), onSuccess callbackkel — abban ugyanúgy setUserSession-t hívsz. Ez az egyik legnagyobb kényelmi különbség a passporthoz képest: nincs strategy-konfiguráció, csak egy handler-fájl.

10.4Jelszókezelés Workers-en — ami 2026-ra megváltozott

Ha korábban olvastál Workers-es auth-ról, valószínűleg ezt láttad: „a bcrypt nem működik, használj Web Crypto PBKDF2-t”. Ez ma már árnyaltabb:

MegoldásWorkers-enMegjegyzés
bcrypt (npm, natív addon)Nem működikC++ bindingokra épül — natív addon soha nem fut a workerd-ben
bcryptjs (tiszta JS)Működik, de drágaLassú, és a Workers CPU-időt számol. Migrációra jó, alapnak nem
node:crypto scrypt / pbkdf2MűködikA nodejs_compat alatt a teljes node:crypto API elérhető (2025 áprilisa óta)
Web Crypto PBKDF2MűködikNatív, mindig elérhető, nem kell hozzá nodejs_compat
nuxt-auth-utils hashPassword/verifyPasswordMűködikscryptre épül — a fenti változás miatt ma használható Workers-en is

Vagyis a legegyszerűbb út ma az, hogy a modul beépített segédfüggvényeit használod:

// regisztráció
const passwordHash = await hashPassword(password)

// bejelentkezés
const ok = await verifyPassword(user.passwordHash, password)

// és ha a hash-paraméterek elavultak, csendben frissítjük sikeres belépéskor
if (ok && passwordNeedsRehash(user.passwordHash)) {
  await db.update(users)
    .set({ passwordHash: await hashPassword(password) })
    .where(eq(users.id, user.id))
}

Migráció a régi bcrypt-hash-ekről

Ha a mostani Express-appod bcrypttel hasheli a jelszavakat, a hash-eket nem tudod „átalakítani” — csak bejelentkezéskor tudsz újrahashelni, mert csak akkor van meg a nyílt jelszó:

async function checkAndMigrate(db, user, password: string) {
  // ① régi formátum? ellenőrizzük bcryptjs-sel (drága, de csak átmenetileg)
  if (user.passwordHash.startsWith('$2')) {
    const bcrypt = await import('bcryptjs')          // dinamikus import: ne nőjön a bundle
    if (!(await bcrypt.compare(password, user.passwordHash))) return false

    // ② sikeres belépés → átírjuk az új formátumra
    await db.update(users)
      .set({ passwordHash: await hashPassword(password) })
      .where(eq(users.id, user.id))
    return true
  }

  return await verifyPassword(user.passwordHash, password)
}
CPU-idő szempont. A jelszó-hashelés szándékosan lassú — ez a lényege. Workers-en ez CPU-időben mérhető költség, de a nagyságrend rendben van: egy hash tipikusan tíz-száz ezredmásodperc, és csak bejelentkezéskor fut. Amire figyelj: ne futtass jelszó-hashelést olyan végponton, amit bárki, hitelesítés nélkül, korlátlanul hívhat — az így elérhető CPU-fogyasztás önmagában DoS-vektor. Rate limitet tegyél a login és a regisztráció elé (Cloudflare-anyag 22. modul).

10.5A védelem középponti mintája

Itt jön össze a 4., 6. és 8. modul. Három réteg, és mindegyiknek külön dolga van:

// server/utils/auth.ts — minden védett végpont ezzel kezdődik

export async function requireSession(event: H3Event) {
  const session = await requireUserSession(event)   // 401, ha nincs
  return {
    userId:   session.user.id,
    tenantId: session.tenantId as string,
    role:     session.role as Role,
  }
}

// és a tenant-határ: a session tenantja EGYEZZEN az útvonaléval
export async function requireTenant(event: H3Event) {
  const s = await requireSession(event)
  const routeTenant = event.context.tenant       // a 4. modul middleware-éből

  if (routeTenant && routeTenant.id !== s.tenantId) {
    // NEM 403 — a 404 nem árulja el, hogy létezik-e az a munkaterület
    throw createError({ statusCode: 404, statusMessage: 'Nem található' })
  }
  return s
}

export function requireRole(s: { role: Role }, needed: Role) {
  if (!hasAtLeast(s.role, needed)) {
    // itt viszont 403 a helyes: a tenanton BELÜL nincs jogosultsága
    throw createError({ statusCode: 403, statusMessage: 'Nincs jogosultság' })
  }
}
// server/api/projects/[id].delete.ts — így néz ki egy védett végpont
export default defineEventHandler(async (event) => {
  const s = await requireTenant(event)
  requireRole(s, 'admin')

  const deleted = await useDb(event).delete(projects)
    .where(and(
      eq(projects.id, getRouterParam(event, 'id')!),
      eq(projects.tenantId, s.tenantId),      // ← a tenant-feltétel MINDIG a lekérdezésben
    ))
    .returning({ id: projects.id })

  if (!deleted.length) throw createError({ statusCode: 404 })
  setResponseStatus(event, 204)
})
A három réteg, és hogy melyik mit véd. ① Az app/middleware/auth.ts route middleware azt éri el, hogy a felhasználó ne lásson olyan képernyőt, amihez nincs joga — kliensen fut, tehát megkerülhető, nem védelem. ② A server/middleware/tenant.ts feloldja a kontextust — nem ellenőriz jogot. ③ A requireTenant() minden egyes végponton — ez a védelem. Ha valahol kimarad, az a végpont védtelen, függetlenül attól, hogy a UI mit mutat.

10.6CSRF

Cookie-alapú session-nél ez nem opcionális. Két lépés, és mindkettő olcsó:

// ① a cookie-beállítás — nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    session: {
      name: '__Host-session',      // __Host- prefix: csak HTTPS, path=/, nincs domain
      cookie: {
        sameSite: 'lax',           // alapvédelem a keresztoldali POST ellen
        secure:   true,
        httpOnly: true,            // JS-ből nem olvasható
        path:     '/',
      },
    },
  },
})
// ② Origin-ellenőrzés minden mutáló kérésre — server/middleware/3.csrf.ts
const SAFE = new Set(['GET', 'HEAD', 'OPTIONS'])

export default defineEventHandler((event) => {
  if (SAFE.has(event.method)) return
  if (!event.path.startsWith('/api/')) return

  const origin = getHeader(event, 'origin')
  const host   = getRequestHost(event)

  // a böngésző mutáló kérésnél mindig küld Origint
  if (!origin || new URL(origin).host !== host) {
    throw createError({ statusCode: 403, statusMessage: 'Érvénytelen eredet' })
  }
})
És egy szabály, ami mindennél többet ér: GET-kérés soha ne mutáljon állapotot. A SameSite=Lax ugyanis engedi a felső szintű GET-navigációt más oldalról — vagyis egy <img src="/api/projects/42/delete"> típusú támadás pontosan azon a résen jön be. Ha a törlés DELETE vagy POST, ez a vektor eleve nem létezik.

10.7Kijelentkezés és session-visszavonás

A lezárt cookie egyetlen komoly gyengéje: amíg le nem jár, érvényes marad, akkor is, ha közben letiltottad a felhasználót. A „jelszóváltás után jelentkezzen ki mindenhonnan” követelményhez kell egy visszavonási mechanizmus. A legolcsóbb a revocation epoch:

// a users táblában van egy sessionEpoch INTEGER mező (alapból 0)
// jelszóváltás, kirúgás, „kilépés minden eszközön" → epoch++

// server/utils/auth.ts
export async function requireSession(event: H3Event) {
  const session = await requireUserSession(event)

  // nem minden kérésnél ellenőrzünk — csak ha a session „öreg"
  const ageMs = Date.now() - (session.checkedAt ?? session.loggedInAt)
  if (ageMs > 5 * 60_000) {
    const current = await getUserEpoch(event, session.user.id)   // KV vagy DB, cache-elve
    if (current !== session.epoch) {
      await clearUserSession(event)
      throw createError({ statusCode: 401, statusMessage: 'A munkamenet lejárt' })
    }
    await setUserSession(event, { checkedAt: Date.now() })    // merge, nem csere
  }

  return { userId: session.user.id, tenantId: session.tenantId, role: session.role }
}

Az ötperces ablak tudatos kompromisszum: a kérések túlnyomó része nulla extra I/O-val megy, a visszavonás pedig legfeljebb öt percen belül érvényesül. Ha ennél szigorúbb kell (például pénzügyi művelet előtt), akkor arra az egy végpontra kényszerítsd ki az azonnali ellenőrzést.

10.8Mi működik a könyvtárakból

CsomagWorkers-enMegjegyzés
nuxt-auth-utilsIgenErre épül ez a modul. Sealed cookie, OAuth-handlerek, jelszó-helperek
better-authIgenTöbb képesség (szervezetek, 2FA, eszközkezelés) — nagyobb felület, több konfiguráció
joseIgenJWT/JWS/JWE Web Cryptóval — ez kell, ha tokent állítasz ki vagy ellenőrzöl
jsonwebtokenKerüldNode-crypto-ra épül; jose a natív megfelelője
passport + strategy-kNemExpress-middleware-modellre épül
express-session, connect-*NemUgyanaz az ok
bcrypt (natív)NemNatív addon. bcryptjs megy, de drága — csak migrációra

Enterprise SSO — amit már most érdemes előkészíteni

Ha B2B SaaS-t építesz, előbb-utóbb jön a „mi Okta/Entra ID-vel akarunk belépni” kérés. Két olcsó döntés most:

10.9Éles indulás előtti checklist

Session-titok secretként van beállítva (nem vars), és környezetenként más
Cookie: httpOnly, secure, sameSite: 'lax', __Host- prefix
Minden védett server/api végpont requireTenant()-tel kezdődik — nem csak a UI van védve
A tenant-feltétel a lekérdezésben van, nem a válasz szűrésében
404, nem 403, ha az erőforrás más tenanthoz tartozik
Rate limit a login, a regisztráció és a jelszó-visszaállítás előtt
GET nem mutál semmit, és van Origin-ellenőrzés a mutáló kérésekre
Visszavonás működik: jelszóváltás után a régi session-ök elévülnek
A session tartalma minimális — és nincs benne olyan, amit a felhasználó ne láthatna (payload!)
A hibaüzenetek nem árulkodnak: „hibás email vagy jelszó”, nem „nincs ilyen felhasználó”
Mi jön ezután? A 11. modul zárja a szerveroldali blokkot: hogyan éred el az adatbázist a server/-ből — bindingok, Drizzle D1-gyel és Hyperdrive-val, a kapcsolat életciklusa kérésenként, és a vasszabály arról, mit nem szabad importálni a szerverkódba.

10.10Ellenőrizd magad

  1. Miért nem működik az express-session + Redis minta, és mi a legkisebb lépés helyette?
    Válasz

    Három ok: h3-ban nincs req/res middleware-lánc; a memóriában tartott store az izolátumok között megosztott lenne (adatszivárgás); és Workers-ből nincs nyers TCP Redishez. A legkisebb lépés a lezárt cookie (nuxt-auth-utils): nincs szerveroldali tároló, és a normál kérés nulla extra I/O-t igényel.

  2. Mi került bele a session-be, és mi nem — és miért?
    Válasz

    Bele: felhasználó- és tenant-azonosító, szerepkör, epoch, időbélyeg. Nem: profil, jogosultsági mátrix, tenant-beállítások. Két ok: a cookie minden kérésben utazik (méret és sávszélesség), és a session tartalma SSR-nél a payloadba is bekerülhet, tehát a felhasználó látja.

  3. Mi változott 2026-ra a jelszó-hasheléssel Workers-en?
    Válasz

    A nodejs_compat alatt elérhetővé vált a teljes node:crypto API (2025 áprilisától), tehát a scrypt és a pbkdf2 natívan használható — így a nuxt-auth-utils beépített hashPassword/verifyPassword párosa is működik. A natív bcrypt npm-csomag továbbra sem fut (C++ addon); a bcryptjs igen, de CPU-drága — csak a régi hash-ek migrálására.

  4. Melyik a három védelmi réteg, és melyik közülük a valódi védelem?
    Válasz

    ① Route middleware a kliensen — csak UX, megkerülhető. ② Szerver middleware a tenant feloldására — kontextust ad, nem ellenőriz jogot. ③ requireTenant() minden végponton — ez a védelem. Ha egy végpontról lemarad, az védtelen, függetlenül attól, hogy a UI mit mutat.

  5. Miért különösen fontos, hogy a GET-kérések ne mutáljanak?
    Válasz

    Mert a SameSite=Lax engedi a felső szintű GET-navigációt más oldalról — vagyis egy idegen oldalról indított GET a felhasználó cookie-jával megy. Ha a mutáló műveletek POST/DELETE/PATCH-en vannak, ez a támadási vektor eleve nem létezik; a SameSite és az Origin-ellenőrzés pedig a maradékot fedi le.

  6. Hogyan lehet visszavonni egy lezárt cookie-session-t anélkül, hogy minden kérésnél adatbázishoz nyúlnál?
    Válasz

    Revocation epoch: a felhasználónak van egy számlálója, ami jelszóváltásnál vagy kirúgásnál nő. A session tartalmazza az akkori értéket, és a szerver csak akkor ellenőrzi (KV-ből vagy cache-elt DB-ből), ha a session „öregebb” egy küszöbnél — például öt percnél. Így a kérések többsége nulla extra I/O, a visszavonás pedig legfeljebb öt percen belül érvényesül.

Előző9. modul — A nagy döntés: full-stack vagy BFF Következő 11. modul — Adatbázis és bindings a server/-ben