Saltar al contenido principal

Restablecer Contraseña

Completa el proceso de restablecimiento de contraseña utilizando el token único recibido por email. El token es válido por 10 minutos y de un solo uso.


POST /auth/reset-password

Información General

Este endpoint permite a los usuarios establecer una nueva contraseña utilizando el token de restablecimiento enviado a su correo electrónico.

POST/auth/reset-password

Actualiza la contraseña del usuario con el token de restablecimiento

📋 Parámetros

tokenstringrequerido

Token UUID de restablecimiento recibido por email

passwordstringrequerido

Nueva contraseña que cumple con requisitos de seguridad

📤 Respuesta

{
"code": 1003,
"message": "Password updated successfully",
"data": {
  "status": "success"
}
}

Requisitos de Contraseña

La nueva contraseña debe cumplir con los siguientes requisitos de seguridad:

Requisitos Obligatorios

Regex de validación: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_]).{9,}$/

  • Mínimo 9 caracteres
  • Al menos 1 letra minúscula (a-z)
  • Al menos 1 letra mayúscula (A-Z)
  • Al menos 1 dígito (0-9)
  • Al menos 1 carácter especial (!@#$%^&*()_+-=[]|;:',.<>?/)

Ejemplos válidos:

  • MiPassword123!
  • SecurePass2024@
  • MyP@ssw0rd!

Ejemplos inválidos:

  • password (falta mayúscula, número y símbolo)
  • Password123 (falta carácter especial)
  • Pass123! (menos de 9 caracteres)

Respuestas

Respuesta Exitosa

Contraseña Actualizada Exitosamente

Código 1003 - Contraseña actualizada correctamente.

{
"code": 1003,
"message": "Password updated successfully",
"data": {
"status": "success"
}
}

Acciones del sistema:

  • Nueva contraseña hasheada con bcrypt
  • Campo password actualizado en MongoDB
  • Campo updated_at actualizado con timestamp actual
  • Token eliminado de Redis (reset:{token})
  • Evento password_reset_execute registrado en logs
  • Token invalidado permanentemente (no puede reutilizarse)

Errores

Token Faltante

Código 4016 - El campo token no fue enviado.

{
"code": 4016,
"message": "Token is required"
}

Causa: El campo token no está presente en el body del request.

Token Inválido o Expirado

Código 4015 - El token no existe, expiró o ya fue utilizado.

{
"code": 4015,
"message": "Invalid or expired token"
}

Causas posibles:

  • Token no existe en Redis (nunca fue generado)
  • Token expiró (más de 10 minutos desde generación)
  • Token ya fue utilizado y eliminado
  • Token malformado o corrupto

Solución: Solicitar un nuevo enlace de restablecimiento usando /auth/forgot-password

Contraseña No Cumple Requisitos

Código 4017 - La contraseña no cumple requisitos de seguridad.

{
"code": 4017,
"message": "Password does not meet security requirements"
}

Causas posibles:

  • Menos de 9 caracteres
  • Falta letra minúscula
  • Falta letra mayúscula
  • Falta número
  • Falta carácter especial

Contraseña Igual a la Actual

Código 4029 - La nueva contraseña es igual a la actual.

{
"code": 4029,
"message": "New password cannot be the same as current password"
}

Causa: La nueva contraseña es idéntica a la contraseña actual del usuario (validación con bcrypt).

Solución: Elegir una contraseña diferente a la actual.

Datos Faltantes o Inválidos

Código 4006 - Faltan campos requeridos o datos inválidos.

{
"code": 4006,
"message": "Missing or invalid data"
}

Causas posibles:

  • Campo password vacío o no enviado
  • Formato de contraseña inválido

Usuario No Encontrado

Código 4001 - El usuario asociado al token no existe.

{
"code": 4001,
"message": "User not found"
}

Causa: El email asociado al token no existe en la base de datos.

Solución: Contactar soporte si el problema persiste.


Proceso Interno del Sistema

Flujo de Operación

sequenceDiagram
participant Client
participant API
participant Redis
participant MongoDB

Client->>API: POST /auth/reset-password
API->>API: Validar token presente

alt Token faltante
API->>Client: 400 (code: 4016)
end

API->>API: Validar formato contraseña

alt Contraseña inválida
API->>Redis: Verificar token válido
alt Token inválido
API->>Client: 400 (code: 4015)
end
API->>Client: 400 (code: 4017)
end

API->>Redis: Buscar email por token

alt Token no existe/expirado
API->>Client: 400 (code: 4015)
end

API->>MongoDB: Buscar usuario por email

alt Usuario no encontrado
API->>Client: 404 (code: 4001)
end

API->>API: Comparar con contraseña actual

alt Contraseña igual
API->>Client: 400 (code: 4029)
end

API->>API: Hash nueva contraseña (bcrypt)
API->>MongoDB: Actualizar password + updated_at
API->>Redis: Eliminar token reset:{token}
API->>Client: 200 (code: 1003)

Note over Client,MongoDB: Token invalidado, contraseña actualizada

Pasos del Proceso

  1. Validación de Token: Verifica que el campo token esté presente (código 4016 si falta)
  2. Validación de Contraseña: Valida formato con regex (código 4017 si inválida, pero primero verifica token)
  3. Recuperación de Email: Busca en Redis la clave reset:{token} (código 4015 si no existe)
  4. Búsqueda de Usuario: Busca usuario en MongoDB por email (código 4001 si no existe)
  5. Validación de Duplicado: Compara nueva contraseña con actual usando bcrypt (código 4029 si son iguales)
  6. Hash de Contraseña: Hashea nueva contraseña con bcrypt
  7. Actualización en DB: Actualiza campos password y updated_at
  8. Limpieza de Token: Elimina token de Redis
  9. Respuesta Exitosa: Retorna código 1003

Ejemplos de Implementación

JavaScript/TypeScript

async function resetPassword(token, newPassword) {
try {
const response = await fetch('https://api.swapbits.co/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: token,
password: newPassword
})
});

const result = await response.json();

if (result.code === 1003) {
console.log('Contraseña actualizada exitosamente');
return {
success: true,
message: 'Contraseña actualizada. Puedes iniciar sesión ahora.'
};
} else if (result.code === 4016) {
return { success: false, error: 'TOKEN_MISSING', message: 'Token faltante' };
} else if (result.code === 4015) {
return {
success: false,
error: 'TOKEN_INVALID',
message: 'Token inválido o expirado. Solicita un nuevo enlace.'
};
} else if (result.code === 4017) {
return {
success: false,
error: 'PASSWORD_WEAK',
message: 'La contraseña no cumple con los requisitos de seguridad'
};
} else if (result.code === 4029) {
return {
success: false,
error: 'PASSWORD_SAME',
message: 'La nueva contraseña no puede ser igual a la actual'
};
}

return { success: false, error: result.message };
} catch (error) {
console.error('Error de red:', error);
return { success: false, error: error.message };
}
}

// Validador de contraseña
const validatePassword = (password) => {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{9,}$/;
return regex.test(password);
};

// Uso
const token = new URLSearchParams(window.location.search).get('token');
const password = 'MiNuevaPassword123!';

if (validatePassword(password)) {
const result = await resetPassword(token, password);
if (result.success) {
alert(result.message);
window.location.href = '/login';
} else {
alert(result.message);
}
} else {
alert('La contraseña no cumple con los requisitos de seguridad');
}

TypeScript con Hook de React

import { useState } from 'react';
import axios from 'axios';

interface ResetPasswordState {
loading: boolean;
error: string | null;
success: boolean;
}

interface PasswordValidation {
valid: boolean;
errors: string[];
}

export function useResetPassword() {
const [state, setState] = useState<ResetPasswordState>({
loading: false,
error: null,
success: false
});

const validatePassword = (password: string): PasswordValidation => {
const errors: string[] = [];

if (password.length < 9) {
errors.push('Mínimo 9 caracteres');
}
if (!/[a-z]/.test(password)) {
errors.push('Incluir minúscula');
}
if (!/[A-Z]/.test(password)) {
errors.push('Incluir mayúscula');
}
if (!/\d/.test(password)) {
errors.push('Incluir número');
}
if (!/[\W_]/.test(password)) {
errors.push('Incluir carácter especial');
}

return { valid: errors.length === 0, errors };
};

const resetPassword = async (token: string, password: string) => {
setState({ loading: true, error: null, success: false });

// Validar contraseña localmente primero
const validation = validatePassword(password);
if (!validation.valid) {
setState({
loading: false,
error: 'Contraseña inválida: ' + validation.errors.join(', '),
success: false
});
return false;
}

try {
const response = await axios.post(
'https://api.swapbits.co/auth/reset-password',
{ token, password }
);

if (response.data.code === 1003) {
setState({ loading: false, error: null, success: true });
return true;
} else {
throw new Error('Respuesta inesperada del servidor');
}
} catch (error: any) {
const errorCode = error.response?.data?.code;
let errorMessage = error.response?.data?.message || 'Error al restablecer contraseña';

// Mensajes personalizados
if (errorCode === 4015) {
errorMessage = 'El enlace ha expirado o es inválido. Solicita uno nuevo.';
} else if (errorCode === 4017) {
errorMessage = 'La contraseña no cumple con los requisitos de seguridad';
} else if (errorCode === 4029) {
errorMessage = 'La nueva contraseña no puede ser igual a la actual';
}

setState({ loading: false, error: errorMessage, success: false });
return false;
}
};

const reset = () => {
setState({ loading: false, error: null, success: false });
};

return { ...state, resetPassword, validatePassword, reset };
}

// Componente con validación en tiempo real
function ResetPasswordForm({ token }: { token: string }) {
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const { loading, error, success, resetPassword, validatePassword } = useResetPassword();

const validation = validatePassword(password);
const passwordsMatch = password === confirmPassword && password.length > 0;

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!passwordsMatch) {
alert('Las contraseñas no coinciden');
return;
}

const success = await resetPassword(token, password);
if (success) {
setTimeout(() => {
window.location.href = '/login';
}, 2000);
}
};

return (
<form onSubmit={handleSubmit}>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Nueva contraseña"
required
minLength={9}
/>

{password.length > 0 && (
<ul style={{ fontSize: '12px', marginTop: '5px' }}>
<li style={{ color: password.length >= 9 ? 'green' : 'red' }}>
{password.length >= 9 ? '✓' : '✗'} Mínimo 9 caracteres
</li>
<li style={{ color: /[a-z]/.test(password) ? 'green' : 'red' }}>
{/[a-z]/.test(password) ? '✓' : '✗'} Una minúscula
</li>
<li style={{ color: /[A-Z]/.test(password) ? 'green' : 'red' }}>
{/[A-Z]/.test(password) ? '✓' : '✗'} Una mayúscula
</li>
<li style={{ color: /\d/.test(password) ? 'green' : 'red' }}>
{/\d/.test(password) ? '✓' : '✗'} Un número
</li>
<li style={{ color: /[\W_]/.test(password) ? 'green' : 'red' }}>
{/[\W_]/.test(password) ? '✓' : '✗'} Un carácter especial
</li>
</ul>
)}
</div>

<div>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirmar contraseña"
required
/>
{confirmPassword.length > 0 && (
<span style={{
fontSize: '12px',
color: passwordsMatch ? 'green' : 'red',
marginTop: '5px',
display: 'block'
}}>
{passwordsMatch ? '✓ Las contraseñas coinciden' : '✗ Las contraseñas no coinciden'}
</span>
)}
</div>

<button
type="submit"
disabled={loading || !validation.valid || !passwordsMatch}
>
{loading ? 'Restableciendo...' : 'Restablecer Contraseña'}
</button>

{success && (
<p style={{ color: 'green' }}>
Contraseña actualizada correctamente. Redirigiendo al login...
</p>
)}

{error && (
<p style={{ color: 'red' }}>Error: {error}</p>
)}
</form>
);
}

Python

import requests
import re
from typing import Dict, Any, List

def validate_password_strength(password: str) -> Dict[str, Any]:
"""
Valida la fortaleza de la contraseña según requisitos

Returns:
Dict con 'valid' (bool) y 'errors' (List[str])
"""
errors = []

if len(password) < 9:
errors.append('Debe tener al menos 9 caracteres')
if not re.search(r'[a-z]', password):
errors.append('Debe contener al menos una letra minúscula')
if not re.search(r'[A-Z]', password):
errors.append('Debe contener al menos una letra mayúscula')
if not re.search(r'\d', password):
errors.append('Debe contener al menos un número')
if not re.search(r'[\W_]', password):
errors.append('Debe contener al menos un carácter especial')

return {
'valid': len(errors) == 0,
'errors': errors
}

def reset_password(token: str, new_password: str) -> Dict[str, Any]:
"""
Restablece la contraseña del usuario

Args:
token: Token de restablecimiento recibido por email
new_password: Nueva contraseña que cumple requisitos

Returns:
Respuesta del servidor con código y mensaje

Raises:
ValueError: Si los parámetros son inválidos
Exception: Si hay error en la solicitud
"""
# Validar parámetros
if not token:
raise ValueError('Token es requerido')

if not new_password:
raise ValueError('Contraseña es requerida')

# Validar fortaleza de contraseña
validation = validate_password_strength(new_password)
if not validation['valid']:
raise ValueError('Contraseña inválida:\n' + '\n'.join(validation['errors']))

url = 'https://api.swapbits.co/auth/reset-password'
data = {
'token': token,
'password': new_password
}

try:
response = requests.post(url, json=data, timeout=10)
response_data = response.json()

# Manejar respuesta exitosa
if response_data.get('code') == 1003:
print("Contraseña actualizada exitosamente")
return {
'success': True,
'message': 'Contraseña actualizada. Puedes iniciar sesión ahora.'
}

# Manejar errores específicos
error_code = response_data.get('code')
error_message = response_data.get('message', 'Error desconocido')

error_messages = {
4016: 'Token faltante',
4015: 'Token inválido o expirado. Solicita un nuevo enlace.',
4017: 'La contraseña no cumple con los requisitos de seguridad',
4029: 'La nueva contraseña no puede ser igual a la actual',
4001: 'Usuario no encontrado',
4006: 'Datos faltantes o inválidos'
}

return {
'success': False,
'error': error_messages.get(error_code, error_message),
'code': error_code
}

except requests.exceptions.Timeout:
return {
'success': False,
'error': 'TIMEOUT',
'message': 'Tiempo de espera agotado'
}
except requests.exceptions.RequestException as e:
return {
'success': False,
'error': 'NETWORK_ERROR',
'message': str(e)
}

# Ejemplo de uso
if __name__ == '__main__':
try:
token = 'token-from-email-link'
new_password = 'MiNuevaPassword123!@'

result = reset_password(token, new_password)

if result['success']:
print(f"\nÉxito: {result['message']}")
else:
print(f"\nError: {result['error']}")
except Exception as e:
print(f"Error: {e}")

cURL

# Restablecimiento básico
curl -X POST \
'https://api.swapbits.co/auth/reset-password' \
-H 'Content-Type: application/json' \
-d '{
"token": "550e8400-e29b-41d4-a716-446655440000",
"password": "NuevaPassword123!@"
}'
# Script bash con validación y manejo de respuesta
#!/bin/bash

TOKEN="$1"
PASSWORD="$2"

if [ -z "$TOKEN" ] || [ -z "$PASSWORD" ]; then
echo "Uso: $0 <token> <password>"
exit 1
fi

# Validar longitud mínima
if [ ${#PASSWORD} -lt 9 ]; then
echo "Error: La contraseña debe tener al menos 9 caracteres"
exit 1
fi

# Hacer request
response=$(curl -s -X POST \
'https://api.swapbits.co/auth/reset-password' \
-H 'Content-Type: application/json' \
-d "{\"token\": \"$TOKEN\", \"password\": \"$PASSWORD\"}")

code=$(echo $response | jq -r '.code')
message=$(echo $response | jq -r '.message')

case $code in
1003)
echo "Éxito: Contraseña actualizada correctamente"
echo "Ahora puedes iniciar sesión con tu nueva contraseña"
;;
4016)
echo "Error: Token faltante"
;;
4015)
echo "Error: Token inválido o expirado"
echo "Solicita un nuevo enlace de restablecimiento"
;;
4017)
echo "Error: La contraseña no cumple con los requisitos de seguridad"
;;
4029)
echo "Error: La nueva contraseña no puede ser igual a la actual"
;;
*)
echo "Error: $message"
;;
esac

GET /auth/reset-password (Validación y Redirección)

Este endpoint complementario valida el token de restablecimiento y redirige al usuario al formulario apropiado.

Información General

GET/auth/reset-password?token={token}

Valida el token y redirige al formulario de restablecimiento

📋 Parámetros

tokenstringrequerido

Token UUID de restablecimiento

📤 Respuesta

HTTP 302 Redirect

Comportamiento de Redirección

graph TD
A[GET /auth/reset-password?token=xxx] --> B{Token presente?}
B -->|No| C[Redirect: /reset-password?error=missing_token]
B -->|Sí| D{Token válido en Redis?}
D -->|No| E[Redirect: /reset-password?error=invalid_token]
D -->|Sí| F[Redirect: /reset-password?token=xxx]

URLs de Redirección

CondiciónURL de Redirección
Token faltantehttps://www.swapbits.io/reset-password?error=missing_token
Token inválido/expiradohttps://www.swapbits.io/reset-password?error=invalid_token
Token válidohttp://localhost:5173/reset-password?token={token}

Consideraciones de Seguridad

Protección del Token

Mejores prácticas para manejo de tokens:

  1. Almacenamiento: No guardar tokens en localStorage; usar solo en memoria o sessionStorage
  2. Transmisión: Siempre usar HTTPS para proteger el token en tránsito
  3. Validación: Validar token inmediatamente antes de mostrar formulario
  4. Expiración: Informar al usuario del tiempo de validez (10 minutos)
  5. Limpieza: Eliminar token de memoria después de uso exitoso o error fatal

Validación de Contraseña en Frontend

Validador completo de contraseña:

class PasswordValidator {
private static readonly PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{9,}$/;

static validate(password: string): { valid: boolean; errors: string[] } {
const errors: string[] = [];

if (password.length < 9) {
errors.push('La contraseña debe tener al menos 9 caracteres');
}
if (!/[a-z]/.test(password)) {
errors.push('Debe incluir al menos una letra minúscula');
}
if (!/[A-Z]/.test(password)) {
errors.push('Debe incluir al menos una letra mayúscula');
}
if (!/\d/.test(password)) {
errors.push('Debe incluir al menos un número');
}
if (!/[\W_]/.test(password)) {
errors.push('Debe incluir al menos un carácter especial');
}

return { valid: errors.length === 0, errors };
}
}

Prevención de Reutilización

Sistema de validación de contraseña duplicada:

El sistema compara la nueva contraseña con la actual usando bcrypt antes de permitir la actualización. Esto previene que usuarios reutilicen la misma contraseña, mejorando la seguridad en caso de que un atacante tenga acceso al enlace de restablecimiento.

Flujo de validación:

  1. Recuperar hash de contraseña actual de MongoDB
  2. Comparar nueva contraseña con hash actual usando bcrypt
  3. Si son iguales, retornar código 4029
  4. Si son diferentes, proceder con actualización

Características del Sistema

Token de Restablecimiento

Especificaciones técnicas:

  • Tipo: UUID v4 único
  • Almacenamiento: Redis con clave reset:{token}
  • Valor: Email del usuario
  • TTL: 10 minutos (600,000 ms)
  • Uso: Un solo uso por token
  • Eliminación: Automática después de uso exitoso

Actualización de Contraseña

Proceso de actualización:

  • Hashing: bcrypt con salt automático
  • Campos actualizados: password, updated_at
  • Validación previa: Compara con contraseña actual
  • Auditoría: Evento password_reset_execute registrado
  • Limpieza: Token eliminado de Redis inmediatamente

Flujo Completo del Usuario

graph TD
A[Usuario recibe email] --> B[Click en enlace reset]
B --> C[GET /auth/reset-password?token=xxx]
C --> D{Token válido?}
D -->|No| E[Redirect error=invalid_token]
D -->|Sí| F[Redirect a formulario con token]
F --> G[Usuario ingresa nueva contraseña]
G --> H{Cumple requisitos?}
H -->|No| I[Mostrar errores validación]
H -->|Sí| J[POST /auth/reset-password]
J --> K{Contraseña diferente?}
K -->|No| L[Error 4029]
K -->|Sí| M[Actualizar en DB]
M --> N[Eliminar token Redis]
N --> O[Respuesta 1003]
O --> P[Redirect a login]

Códigos de Respuesta

CódigoHTTP StatusDescripción
1003200Contraseña actualizada exitosamente
4016400Token faltante en request
4015400Token inválido o expirado
4017400Contraseña no cumple requisitos
4029400Nueva contraseña igual a la actual
4006400Datos faltantes o inválidos
4001404Usuario no encontrado

Notas Importantes

Información Clave

Puntos importantes a recordar:

  1. Expiración: Token válido por exactamente 10 minutos
  2. Uso Único: Token se elimina después de uso exitoso
  3. Validación Previa: Nueva contraseña no puede ser igual a la actual
  4. Requisitos Estrictos: Mínimo 9 caracteres con mayúscula, minúscula, número y símbolo
  5. Redis Storage: Token en reset:{token} con email como valor
  6. Auditoría: Evento password_reset_execute en logs
  7. Metadata: Campo updated_at actualizado automáticamente
  8. Hashing: bcrypt para almacenamiento seguro

Probar esta API

¿Quieres probar este endpoint de forma interactiva con tus propios datos?


Enlaces Relacionados