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
Validate Input
The system checks that username, email, and password are provided and validates email format.
Check for Duplicates
Queries the database to ensure the username and email aren’t already registered.
Hash Password
Uses bcrypt with 10 salt rounds to securely hash the password.
Create User Record
Inserts the new user into PostgreSQL and returns the user ID.
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' });
}
});
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
Submit Credentials
User provides username and password.
Retrieve User
Query database for user record by username.
Verify Password
Use bcrypt.compare() to validate the password against the stored hash.
Generate Token
Create a JWT token valid for 7 days.
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!"
}
{
"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
API Endpoint
Backend Logic
Frontend
POST / api / users / forgot - password
Content - Type : application / json
{
"email" : "[email protected] "
}
From src/routes/users.js:87-121: router . post ( '/forgot-password' , async ( req , res ) => {
try {
const { email } = req . body ;
if ( ! email ) {
return res . status ( 400 ). json ({ error: 'Correo requerido' });
}
const { rows } = await pool . query (
`SELECT id FROM users WHERE email = $1` ,
[ email ]
);
if ( rows . length === 0 ) {
return res . json ({ message: 'Si el correo existe, recibirás un código de recuperación' });
}
// Generate 6-digit code
const resetCode = Math . random (). toString (). substring ( 2 , 8 );
const expiry = new Date ( Date . now () + 30 * 60 * 1000 ); // 30 minutes
await pool . query (
`UPDATE users SET reset_code = $1, reset_code_expiry = $2 WHERE id = $3` ,
[ resetCode , expiry , rows [ 0 ]. id ]
);
if ( process . env . NODE_ENV !== 'production' ) {
console . log ( `[dev] Código de recuperación para ${ email } : ${ resetCode } ` );
}
return res . json ({ message: 'Si el correo existe, recibirás un código de recuperación' });
} catch ( error ) {
return res . status ( 500 ). json ({ error: 'Error interno del servidor' });
}
});
From src/pages/resetpassword.jsx:29-63: const handleEmailSubmit = async ( e ) => {
e . preventDefault ();
setLoading ( true );
const emailRegex = / ^ [ ^ \s@ ] + @ [ ^ \s@ ] + \. [ ^ \s@ ] + $ / ;
if ( ! emailRegex . test ( email )) {
setMessage ( 'Por favor ingresa un correo electrónico válido' );
return ;
}
try {
const response = await fetch ( ` ${ API_URL } /api/users/forgot-password` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ email })
});
const data = await response . json ();
setMessage ( 'Se ha enviado un código de recuperación a tu correo.' );
setStep ( 2 );
} catch ( error ) {
setMessage ( `Error de conexión: ${ error . message } ` );
} finally {
setLoading ( false );
}
};
Step 2: Verify Recovery Code
API Endpoint
Backend Logic
POST / api / users / verify - reset - code
Content - Type : application / json
{
"email" : "[email protected] " ,
"resetCode" : "123456"
}
From src/routes/users.js:125-152: router . post ( '/verify-reset-code' , async ( req , res ) => {
try {
const { email , resetCode } = req . body ;
if ( ! email || ! resetCode ) {
return res . status ( 400 ). json ({ error: 'Correo y código requeridos' });
}
const { rows } = await pool . query (
`SELECT id, reset_code_expiry FROM users WHERE email = $1 AND reset_code = $2` ,
[ email , resetCode ]
);
if ( rows . length === 0 ) {
return res . status ( 401 ). json ({ error: 'Código de recuperación inválido' });
}
if ( new Date ( rows [ 0 ]. reset_code_expiry ) < new Date ()) {
return res . status ( 401 ). json ({ error: 'El código de recuperación ha expirado' });
}
return res . json ({ message: 'Código verificado exitosamente' , userId: rows [ 0 ]. id });
} catch ( error ) {
return res . status ( 500 ). json ({ error: 'Error interno del servidor' });
}
});
Recovery codes expire after 30 minutes for security. Users must request a new code if their code expires.
Step 3: Reset Password
API Endpoint
Backend Logic
Frontend
POST / api / users / reset - password
Content - Type : application / json
{
"email" : "[email protected] " ,
"resetCode" : "123456" ,
"newPassword" : "NewSecurePass123!"
}
From src/routes/users.js:156-193: router . post ( '/reset-password' , async ( req , res ) => {
try {
const { email , resetCode , newPassword } = req . body ;
if ( ! newPassword || newPassword . length < 6 ) {
return res . status ( 400 ). json ({ error: 'La contraseña debe tener al menos 6 caracteres' });
}
const { rows } = await pool . query (
`SELECT id, reset_code_expiry FROM users WHERE email = $1 AND reset_code = $2` ,
[ email , resetCode ]
);
if ( rows . length === 0 ) {
return res . status ( 401 ). json ({ error: 'Código de recuperación inválido' });
}
if ( new Date ( rows [ 0 ]. reset_code_expiry ) < new Date ()) {
return res . status ( 401 ). json ({ error: 'El código de recuperación ha expirado' });
}
const hashedPassword = await bcrypt . hash ( newPassword , SALT_ROUNDS );
const { rows : updated } = await pool . query (
`UPDATE users
SET password = $1, reset_code = NULL, reset_code_expiry = NULL
WHERE email = $2
RETURNING id, username` ,
[ hashedPassword , email ]
);
return res . json ({
message: 'Contraseña actualizada exitosamente' ,
userId: updated [ 0 ]. id ,
username: updated [ 0 ]. username
});
} catch ( error ) {
return res . status ( 500 ). json ({ error: 'Error interno del servidor' });
}
});
From src/pages/resetpassword.jsx:102-146: const handlePasswordSubmit = async ( e ) => {
e . preventDefault ();
setLoading ( true );
if ( newPassword !== confirmPassword ) {
setMessage ( 'Las contraseñas no coinciden' );
return ;
}
if ( newPassword . length < 6 ) {
setMessage ( 'La contraseña debe tener al menos 6 caracteres' );
return ;
}
const hasSpecialChar = / [ !@#$%^&*()_+\-= \[\] {};':" \\ |,.<> \/ ? ] / . test ( newPassword );
if ( ! hasSpecialChar ) {
setMessage ( 'La contraseña debe contener al menos un carácter especial' );
return ;
}
try {
const response = await fetch ( ` ${ API_URL } /api/users/reset-password` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ email , resetCode , newPassword })
});
const data = await response . json ();
if ( response . ok ) {
setMessage ( '¡Contraseña actualizada exitosamente! Redirigiendo...' );
setTimeout (() => onBack (), 2000 );
}
} catch ( error ) {
setMessage ( `Error de conexión: ${ error . message } ` );
} finally {
setLoading ( false );
}
};
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
User Enumeration Prevention
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
Always use HTTPS in production to prevent token interception
Store tokens securely on the client (httpOnly cookies preferred over localStorage)
Validate input on both frontend and backend
Use environment variables for secrets
Implement rate limiting on authentication endpoints to prevent brute force attacks
Log authentication events for security monitoring