The Clínica Vitalis platform uses JWT (JSON Web Tokens) for stateless authentication. This approach eliminates the need for server-side session storage while providing secure, tamper-proof tokens that can be validated without database lookups.
Token Structure
JWT tokens consist of three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsImlhdCI6MTYxNjIzOTAyMiwiZXhwIjoxNjE2MjUzNDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header
Contains the algorithm (HS256) and token type (JWT)
Payload
Contains the user data claims (id, issued at, expiration)
Signature
Cryptographic signature to verify token authenticity
Decoded Token Example
{
"alg" : "HS256" ,
"typ" : "JWT"
}
Token Generation
Tokens are generated during successful login using the generateJWT helper function:
backend/helpers/generateJWT.ts
import jwt from "jsonwebtoken"
export const generateJWT = ( id : string = '' ) : Promise < string > => {
return new Promise (( res , rej ) => {
const payload = { id }
jwt . sign (
payload ,
process . env . KEY_SECRET as string ,
{
expiresIn: "4h"
},
( err : Error | null , token : string | undefined ) => {
if ( err ) {
console . log ( err );
rej ( "No se pudo generar el JWT" )
} else {
res ( token as string )
}
}
)
})
}
Token Generation Process
Create Payload
Payload contains only the user ID - minimal data to reduce token size
Sign Token
Token is signed with KEY_SECRET environment variable using HS256 algorithm
Set Expiration
Token expires after 4 hours, requiring users to re-authenticate
Return Token
Generated token is returned as a Promise for async handling
Login Flow with Token Generation
backend/controllers/auth.ts
export const login = async ( req : Request , res : Response ) : Promise < void > => {
const { email , password } : IUser = req . body
try {
const user = await User . findOne ({
where: {
email: email
}
})
if ( ! user ) {
res . status ( 400 ). json ({
msg: "No se encontró el email"
})
return
}
const validatePassword = bycryptjs . compareSync ( password , user . password )
if ( ! validatePassword ) {
res . status ( 401 ). json ({
msg: "La contraseña es incorrecta"
})
return
}
const token = await generateJWT ( user . id . toString ())
res . status ( 200 ). json ({
name: user . name ,
surname: user . surname ,
email: user . email ,
rol: user . rol ,
token
})
} catch ( error ) {
console . log ( error );
res . status ( 500 ). json ({
msg: "Error en el servidor"
})
}
}
Token Validation
Protected routes use the validatorJWT middleware to verify tokens:
backend/middlewares/validatorJWT.ts
import { NextFunction , Request , Response } from "express" ;
import jwt , { JwtPayload } from "jsonwebtoken" ;
import { IUser , User } from "../models/user" ;
export const validatorJWT = async ( req : Request , res : Response , next : NextFunction ) => {
const token = req . headers [ "x-token" ] as string
if ( ! token ) {
res . status ( 401 ). json ({
msg: 'No hay token en la petición'
})
return
}
try {
const secret_key = process . env . KEY_SECRET as string ;
const payload = jwt . verify ( token , secret_key ) as JwtPayload ;
const { id } = payload ;
const userConfirmated : IUser | null = await User . findByPk ( id )
if ( ! userConfirmated ) {
res . status ( 404 ). json ({
msg: 'El usuario no se encuentra en la DB'
})
throw new Error ( "Usuario no encontrado" );
}
req . body = { ... req . body , userConfirmated }
next ();
} catch ( error ) {
console . log ( error );
res . status ( 401 ). json ({
msg: 'Token no válido'
})
}
}
Validation Process
Extract Token
Token is read from the x-token header
Verify Signature
JWT library verifies the token signature using KEY_SECRET
Check Expiration
JWT library automatically validates the token hasn’t expired
Confirm User Exists
User ID from payload is used to fetch user from database
Attach User to Request
Validated user object is added to req.body for downstream use
Even with a valid token signature, the middleware performs a database lookup to ensure the user still exists. This prevents deleted users from accessing the system.
Using Protected Routes
Client Implementation
const token = localStorage . getItem ( 'token' );
fetch ( 'http://localhost:3000/api/protected-resource' , {
method: 'GET' ,
headers: {
'Content-Type' : 'application/json' ,
'x-token' : token
}
})
. then ( response => response . json ())
. then ( data => console . log ( data ));
Backend Route Protection
import { Router } from 'express' ;
import { validatorJWT } from '../middlewares/validatorJWT' ;
import { isAdmin } from '../middlewares/validatorAdmin' ;
const router = Router ();
// User-level protection
router . get ( '/appointments' ,
validatorJWT ,
getAppointments
);
// Admin-level protection
router . delete ( '/appointments/:id' ,
[
validatorJWT ,
isAdmin
],
deleteAppointment
);
Token Lifecycle
Token States
State Description Status Code Active Valid token within 4-hour window, user exists 200 Expired Token older than 4 hours 401 Invalid Bad signature or malformed token 401 User Not Found Valid token but user deleted from DB 404 Missing No x-token header provided 401
Security Considerations
Token Storage
Client-Side Storage : Tokens should be stored securely on the client:
localStorage : Simple but vulnerable to XSS attacks
sessionStorage : Cleared when browser closes, slightly more secure
HttpOnly Cookies : Most secure, inaccessible to JavaScript
For production systems, consider using HttpOnly cookies with SameSite attributes.
Secret Key Management
# Strong secret key for JWT signing
KEY_SECRET = your-super-secret-key-min-32-characters-long
# Admin registration key
KEY_FOR_ADMIN = your-admin-key-keep-this-secure
Generate Strong Secret Keys
Use cryptographically secure random strings: # Using Node.js
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# Using OpenSSL
openssl rand -hex 64
When rotating JWT secret keys:
Generate new secret key
Update environment variable
All existing tokens become invalid
Users must re-authenticate
Plan key rotations during low-traffic periods and notify users.
Environment-Specific Keys
Use different keys for each environment:
Development: Can be simpler for testing
Staging: Should mirror production security
Production: Maximum length and complexity
Token Expiration Strategy
The 4-hour expiration balances security and user experience:
Advantages:
Limits damage if token is compromised
Forces periodic re-authentication
Prevents indefinite access
Trade-offs:
Users must log in every 4 hours
Can interrupt long work sessions
Consider implementing refresh tokens for longer sessions while maintaining security:
Short-lived access tokens (15 minutes)
Long-lived refresh tokens (7 days)
Automatic token refresh before expiration
Error Responses
Missing Token
{
"msg" : "No hay token en la petición"
}
Status Code: 401 Unauthorized
Invalid Token
{
"msg" : "Token no válido"
}
Status Code: 401 Unauthorized
Causes:
Token signature doesn’t match
Token has expired
Token is malformed
Secret key has changed
User Not Found
{
"msg" : "El usuario no se encuentra en la DB"
}
Status Code: 404 Not Found
Cause: User was deleted after token was issued
Testing JWT Authentication
Login and Get Token
# Login
TOKEN = $( curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123"
}' | jq -r '.token' )
# Use token for authenticated request
curl -X GET http://localhost:3000/api/appointments \
-H "x-token: $TOKEN "
Decode Token (Without Verification)
const jwt = require ( 'jsonwebtoken' );
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ;
// Decode without verifying (for debugging only)
const decoded = jwt . decode ( token );
console . log ( decoded );
// Output: { id: '123', iat: 1616239022, exp: 1616253422 }
Never use jwt.decode() for authentication - it doesn’t verify the signature. Always use jwt.verify() with your secret key.
Best Practices
DO:
Use strong, random secret keys (64+ characters)
Set appropriate token expiration times
Validate tokens on every protected request
Verify user still exists in database
Use HTTPS to prevent token interception
Store tokens securely on the client
Log authentication failures for monitoring
DON’T:
Store sensitive data in JWT payload (it’s base64, not encrypted)
Use the same secret key across environments
Accept tokens from query parameters (use headers)
Skip token validation for “internal” routes
Log tokens in plain text
Share secret keys in code repositories
Next Steps
Authentication Overview Return to authentication system overview
User Management Learn about roles and permissions