Skip to main content
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
1

Header

Contains the algorithm (HS256) and token type (JWT)
2

Payload

Contains the user data claims (id, issued at, expiration)
3

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

1

Create Payload

Payload contains only the user ID - minimal data to reduce token size
2

Sign Token

Token is signed with KEY_SECRET environment variable using HS256 algorithm
3

Set Expiration

Token expires after 4 hours, requiring users to re-authenticate
4

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

1

Extract Token

Token is read from the x-token header
2

Verify Signature

JWT library verifies the token signature using KEY_SECRET
3

Check Expiration

JWT library automatically validates the token hasn’t expired
4

Confirm User Exists

User ID from payload is used to fetch user from database
5

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

StateDescriptionStatus Code
ActiveValid token within 4-hour window, user exists200
ExpiredToken older than 4 hours401
InvalidBad signature or malformed token401
User Not FoundValid token but user deleted from DB404
MissingNo x-token header provided401

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

.env.example
# 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
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:
  1. Generate new secret key
  2. Update environment variable
  3. All existing tokens become invalid
  4. Users must re-authenticate
Plan key rotations during low-traffic periods and notify users.
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

Build docs developers (and LLMs) love