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.
/auth/reset-passwordActualiza la contraseña del usuario con el token de restablecimiento
📋 Parámetros
tokenstringrequeridoToken UUID de restablecimiento recibido por email
passwordstringrequeridoNueva 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
passwordactualizado en MongoDB - Campo
updated_atactualizado con timestamp actual - Token eliminado de Redis (
reset:{token}) - Evento
password_reset_executeregistrado 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
passwordvací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
- Validación de Token: Verifica que el campo
tokenesté presente (código 4016 si falta) - Validación de Contraseña: Valida formato con regex (código 4017 si inválida, pero primero verifica token)
- Recuperación de Email: Busca en Redis la clave
reset:{token}(código 4015 si no existe) - Búsqueda de Usuario: Busca usuario en MongoDB por email (código 4001 si no existe)
- Validación de Duplicado: Compara nueva contraseña con actual usando bcrypt (código 4029 si son iguales)
- Hash de Contraseña: Hashea nueva contraseña con bcrypt
- Actualización en DB: Actualiza campos
passwordyupdated_at - Limpieza de Token: Elimina token de Redis
- 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
/auth/reset-password?token={token}Valida el token y redirige al formulario de restablecimiento
📋 Parámetros
tokenstringrequeridoToken UUID de restablecimiento
📤 Respuesta
HTTP 302 RedirectComportamiento 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ón | URL de Redirección |
|---|---|
| Token faltante | https://www.swapbits.io/reset-password?error=missing_token |
| Token inválido/expirado | https://www.swapbits.io/reset-password?error=invalid_token |
| Token válido | http://localhost:5173/reset-password?token={token} |
Consideraciones de Seguridad
Protección del Token
Mejores prácticas para manejo de tokens:
- Almacenamiento: No guardar tokens en localStorage; usar solo en memoria o sessionStorage
- Transmisión: Siempre usar HTTPS para proteger el token en tránsito
- Validación: Validar token inmediatamente antes de mostrar formulario
- Expiración: Informar al usuario del tiempo de validez (10 minutos)
- 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:
- Recuperar hash de contraseña actual de MongoDB
- Comparar nueva contraseña con hash actual usando bcrypt
- Si son iguales, retornar código 4029
- 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_executeregistrado - 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ódigo | HTTP Status | Descripción |
|---|---|---|
| 1003 | 200 | Contraseña actualizada exitosamente |
| 4016 | 400 | Token faltante en request |
| 4015 | 400 | Token inválido o expirado |
| 4017 | 400 | Contraseña no cumple requisitos |
| 4029 | 400 | Nueva contraseña igual a la actual |
| 4006 | 400 | Datos faltantes o inválidos |
| 4001 | 404 | Usuario no encontrado |
Notas Importantes
Información Clave
Puntos importantes a recordar:
- Expiración: Token válido por exactamente 10 minutos
- Uso Único: Token se elimina después de uso exitoso
- Validación Previa: Nueva contraseña no puede ser igual a la actual
- Requisitos Estrictos: Mínimo 9 caracteres con mayúscula, minúscula, número y símbolo
- Redis Storage: Token en
reset:{token}con email como valor - Auditoría: Evento
password_reset_executeen logs - Metadata: Campo
updated_atactualizado automáticamente - Hashing: bcrypt para almacenamiento seguro
Probar esta API
¿Quieres probar este endpoint de forma interactiva con tus propios datos?
Enlaces Relacionados
- Solicitar Restablecimiento - Obtener token de restablecimiento
- Solicitar Cambio de Contraseña - Solicitar cambio estando autenticado
- Login - Iniciar sesión después de restablecer
- Registro v1 - Crear nueva cuenta