Skip to main content
MediGuide implements a comprehensive authentication system with JWT tokens, secure password hashing, and a complete password recovery workflow.

Overview

The authentication system provides:
  • User registration with duplicate checks
  • Secure login with JWT token generation
  • Password recovery via email verification codes
  • bcrypt password hashing with 10 salt rounds
  • 7-day token expiration
  • Protected routes with authorization middleware

User Registration

Registration Flow

1

Validate Input

The system checks that username, email, and password are provided and validates email format.
2

Check for Duplicates

Queries the database to ensure the username and email aren’t already registered.
3

Hash Password

Uses bcrypt with 10 salt rounds to securely hash the password.
4

Create User Record

Inserts the new user into PostgreSQL and returns the user ID.
5

Generate JWT Token

Creates a JWT token with 7-day expiration containing the user ID and username.

API Endpoint

POST /api/users/signup
Content-Type: application/json

{
  "username": "john_doe",
  "email": "[email protected]",
  "password": "SecurePass123!"
}

Backend Implementation

From src/routes/users.js:11-47:
router.post('/signup', async (req, res) => {
  try {
    const { username, email, password } = req.body;

    if (!username || !email || !password) {
      return res.status(400).json({ error: 'Todos los campos son requeridos' });
    }

    // Check for existing username or email
    const { rows } = await pool.query(
      `SELECT username, email FROM users WHERE username = $1 OR email = $2`,
      [username, email]
    );

    if (rows.length > 0) {
      const campo = rows[0].username === username ? 'Usuario' : 'Correo';
      return res.status(400).json({ error: `${campo} ya está registrado` });
    }

    // Hash password with bcrypt
    const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

    const { rows: inserted } = await pool.query(
      `INSERT INTO users (username, email, password, created_at)
       VALUES ($1, $2, $3, NOW())
       RETURNING id, username`,
      [username, email, hashedPassword]
    );

    const user = inserted[0];
    const token = jwt.sign(
      { id: user.id, username: user.username },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    return res.status(201).json({
      message: 'Usuario registrado exitosamente',
      token,
      userId: user.id,
      username: user.username
    });
  } catch (error) {
    return res.status(500).json({ error: 'Error interno del servidor' });
  }
});

Frontend Registration Form

From src/pages/userinfo.jsx:53-84:
const response = await fetch(`${API_URL}/api/users/signup`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: formData.username,
    email: formData.email,
    password: formData.password
  })
});

const data = await response.json();

if (response.ok) {
  setMessage('¡Cuenta creada exitosamente! Iniciando sesión...');
  setTimeout(() => onAuthSuccess(data), 1000);
}
Passwords are hashed using bcrypt with 10 salt rounds before storage. The plain text password is never stored in the database.

User Login

Login Process

1

Submit Credentials

User provides username and password.
2

Retrieve User

Query database for user record by username.
3

Verify Password

Use bcrypt.compare() to validate the password against the stored hash.
4

Generate Token

Create a JWT token valid for 7 days.
5

Return Authentication Data

Send token, user ID, and username to the client.

API Endpoint

POST /api/users/login
Content-Type: application/json

{
  "username": "john_doe",
  "password": "SecurePass123!"
}

Response Format

{
  "message": "Inicio de sesión exitoso",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "userId": 42,
  "username": "john_doe"
}

Backend Implementation

From src/routes/users.js:51-83:
router.post('/login', async (req, res) => {
  try {
    const { username, password } = req.body;

    if (!username || !password) {
      return res.status(400).json({ error: 'Usuario y contraseña requeridos' });
    }

    const { rows } = await pool.query(
      `SELECT id, username, password FROM users WHERE username = $1`,
      [username]
    );

    if (rows.length === 0) {
      return res.status(401).json({ error: 'Usuario o contraseña incorrectos' });
    }

    const user = rows[0];
    const passwordMatch = await bcrypt.compare(password, user.password);

    if (!passwordMatch) {
      return res.status(401).json({ error: 'Usuario o contraseña incorrectos' });
    }

    const token = jwt.sign(
      { id: user.id, username: user.username },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    return res.json({
      message: 'Inicio de sesión exitoso',
      token,
      userId: user.id,
      username: user.username
    });
  } catch (error) {
    return res.status(500).json({ error: 'Error interno del servidor' });
  }
});
The same error message is returned for both invalid username and invalid password to prevent user enumeration attacks.

JWT Token Authentication

Token Structure

JWT tokens contain:
  • Payload: User ID and username
  • Expiration: 7 days from creation
  • Signature: Signed with JWT_SECRET environment variable

Authorization Middleware

From src/middleware/auth.js:3-16:
const auth = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'No autorizado: token requerido' });
  }

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ error: 'Token inválido o expirado' });
  }
};

Using Authenticated Endpoints

All medical data routes require authentication:
import auth from '../middleware/auth.js';

const router = Router();

// All medical routes require authentication
router.use(auth);

router.post('/', async (req, res) => {
  // req.user contains the decoded JWT payload
  if (req.user.id !== userId) {
    return res.status(403).json({ error: 'No autorizado' });
  }
  // ... save medical data
});

Making Authenticated Requests

From src/components/medinfo.jsx:28-35:
const response = await fetch(`${API_URL}/api/medical-info`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({ ...formData, userId })
});

Password Recovery System

MediGuide implements a secure 3-step password recovery process:

Step 1: Request Recovery Code

POST /api/users/forgot-password
Content-Type: application/json

{
  "email": "[email protected]"
}

Step 2: Verify Recovery Code

POST /api/users/verify-reset-code
Content-Type: application/json

{
  "email": "[email protected]",
  "resetCode": "123456"
}
Recovery codes expire after 30 minutes for security. Users must request a new code if their code expires.

Step 3: Reset Password

POST /api/users/reset-password
Content-Type: application/json

{
  "email": "[email protected]",
  "resetCode": "123456",
  "newPassword": "NewSecurePass123!"
}

Security Features

MediGuide uses bcrypt with 10 salt rounds to hash passwords before storage. This protects against:
  • Rainbow table attacks
  • Database breaches exposing plain text passwords
  • Brute force attacks (bcrypt is intentionally slow)
const SALT_ROUNDS = 10;
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
Tokens expire after 7 days, requiring users to re-authenticate periodically:
const token = jwt.sign(
  { id: user.id, username: user.username },
  process.env.JWT_SECRET,
  { expiresIn: '7d' }
);
Password recovery codes expire after 30 minutes:
const expiry = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes
The same generic message is returned whether a user exists or not:
return res.json({ message: 'Si el correo existe, recibirás un código de recuperación' });
Users can only access their own data:
if (req.user.id !== userId) {
  return res.status(403).json({ error: 'No autorizado' });
}

Database Schema

From initDb.js:6-14:
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(255) UNIQUE NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL,
  reset_code VARCHAR(10),
  reset_code_expiry TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

Environment Configuration

# Required environment variables
JWT_SECRET=your-secret-key-here
NODE_ENV=production
Never commit your JWT_SECRET to version control. Use environment variables and keep secrets secure.

Best Practices

  1. Always use HTTPS in production to prevent token interception
  2. Store tokens securely on the client (httpOnly cookies preferred over localStorage)
  3. Validate input on both frontend and backend
  4. Use environment variables for secrets
  5. Implement rate limiting on authentication endpoints to prevent brute force attacks
  6. Log authentication events for security monitoring

Build docs developers (and LLMs) love