Skip to main content
Nuxt Secure uses JSON Web Tokens (JWT) for stateless session management combined with Cloudflare Turnstile CAPTCHA to block automated login attempts. Sessions are stored in an HTTP cookie so they survive page refreshes and work during server-side rendering.

Login flow

1

User submits the login form

The login page collects strNombreUsuario (username), strPwd (password), and a Turnstile token generated automatically by the @nuxtjs/turnstile widget when the user solves the challenge.
2

Turnstile token is validated

The API handler posts the token to https://challenges.cloudflare.com/turnstile/v0/siteverify using the server-side TURNSTILE_SECRET_KEY. If validation fails, the request is rejected with HTTP 400 before the database is ever queried.
3

Database lookup by username

Drizzle ORM queries the usuario table using eq(usuario.strNombreUsuario, strNombreUsuario). If no row is found, or the user’s idEstadoUsuario is false (inactive), HTTP 401 is returned.
4

Password verification

bcrypt.compare() compares the submitted plain-text password against the hashed value stored in strPwd. A mismatch returns HTTP 401.
5

JWT is issued

jwt.sign() creates a token containing { id, idPerfil, nombre } signed with JWT_SECRET. The token expires in 8 hours.
6

Token stored in cookie

The client composable writes the token to the auth_token cookie with maxAge: 60 * 60 * 8 (28 800 seconds). useCookie from Nuxt ensures the cookie is accessible both server-side and client-side.
7

User data persisted

The user object (id, nombre, idPerfil, correo, celular, imagenUrl) is serialised to localStorage under the key usuario and stored in useState('usuarioLogueado') for reactive access across all components.
8

Permissions loaded

cargarMisPermisos(idPerfil) fetches /api/permisos/mis-permisos/:idPerfil and stores the result in useState('misPermisos') as a Record<string, PermisosAccion> keyed by module name in uppercase.
9

Redirect to the main page

The router navigates to /principal-1 on success.

Login handler source

server/api/auth/login.post.ts

Global route middleware

app/middleware/auth.global.ts runs before every route navigation — on the server during SSR and on the client during SPA navigation. It enforces three rules:
  1. Root redirect — visiting / always redirects to /login.
  2. Route protection — any route other than /login requires a valid auth_token cookie; missing token redirects to /login.
  3. Double-login prevention — a user who already has a token and tries to visit /login is sent to /principal-1 instead.
app/middleware/auth.global.ts
useCookie works in both SSR and client contexts. Avoid reading auth_token from document.cookie directly — it will not be available during server rendering.

Session persistence

Nuxt’s useState is reset when the page is hard-refreshed (F5). restaurarSesion() in useAuth.ts handles this by reading the user object back from localStorage and re-fetching permissions if the in-memory state is empty:
app/composables/useAuth.ts
Call restaurarSesion() in the onMounted hook of any page or layout that needs reactive user data after a refresh.

JWT token structure

The secret used to sign and verify tokens is read from the JWT_SECRET environment variable via useRuntimeConfig().jwtSecret.

Security notes

The auth_token cookie is not set with httpOnly by default in the current implementation. If you expose this app to the public internet, consider adding the httpOnly and secure flags to prevent client-side JavaScript from reading the token.
  • bcrypt hashing — passwords are never stored in plain text; bcrypt’s work factor makes brute-force attacks computationally expensive.
  • Turnstile CAPTCHA — the Cloudflare challenge runs before any database query, blocking credential-stuffing bots without storing any user fingerprint.
  • Short token lifetime — the 8-hour expiry limits the window of exposure if a token is leaked.
  • Status check — inactive users (idEstadoUsuario: false) are rejected at login without revealing which check failed.