Servir códigos 2FA rotativos a una tienda sin filtrar el secreto
- Publicado el
- • 5 min de lectura•--- vistas
Algunos negocios completan pedidos sobre cuentas protegidas con verificación en dos pasos. La reventa de productos digitales es el caso evidente, pero la forma es general: un operador tiene que entrar en algo en nombre de un cliente, y ese acceso exige un código válido durante treinta segundos.
Hecho a mano, esto es una persona leyendo dígitos de un móvil y pegándolos en un chat. Falla de las maneras previsibles: el código caduca a mitad del traspaso, dos operadores trabajan la misma cuenta a la vez y nadie puede atender a un cliente a las tres de la madrugada.
El instinto es guardar los secretos TOTP en la base de la tienda y generar códigos bajo demanda. No lo hagas. Aquí va una descomposición mejor.
El secreto vive exactamente en un sitio
Un secreto TOTP es una credencial permanente: quien lo tenga puede generar códigos válidos para siempre, sin ningún acceso adicional. Eso lo hace categóricamente distinto de una sesión o un token, que caducan y se pueden revocar.
En el momento en que copias ese secreto a la base de la tienda, se lo has dado a todo lo que puede leerla: un plugin, una copia de seguridad, un clon de pruebas, una inyección SQL en código no relacionado, un colaborador con un volcado. Todos los riesgos habituales de WordPress ascienden de golpe a compromiso permanente de cuentas.
Por eso el plugin no guarda secretos ni lógica de generación. Es cliente de un servicio dedicado:
class CustomTwoFaService extends TwoFaService
{
public function __construct()
{
$endpoint = get_option('tfa_endpoint');
$token = get_option('tfa_token');
if (empty($endpoint) || empty($token)) {
throw new Exception('2FA service endpoint or token is not set');
}
parent::__construct($endpoint, $token);
}
}
Dos opciones y una excepción lanzada. La tienda conoce una dirección y un token; los secretos se quedan detrás del servicio. Cambiar de servicio pasa a ser un cambio de ajustes, y una base de datos comprometida entrega códigos válidos treinta segundos en vez de cuentas comprometidas para siempre.
Emparejar por QR, no tecleando
Registrar una cuenta significa transferir un secreto, y pedirle a un cliente que reescriba una cadena base32 es la forma de generar tickets de soporte. Lee el QR de emparejamiento: en el navegador, con jsQR, desde la cámara o desde una captura subida.
POST /wp-json/2fa/v1/auth-user-by-qr (multipart: qr)
→ decodificar → registrar en el servicio → { account_id, email, code }
La imagen subida se borra inmediatamente después de procesarla. Contiene el secreto, así que no debe sobrevivir a la petición: una imagen QR olvidada en un directorio de subidas es la misma fuga que guardar el secreto, con el aliciente de que el servidor web la sirve públicamente.
Dos consultas, una fuente
Los códigos se exponen por dos endpoints, uno por dirección de cuenta y otro por identificador interno:
| Endpoint | Quién lo usa |
|---|---|
get-2fa-code-by-email | la página del cliente |
get-2fa-code-by-id | herramientas internas |
Ambos leen del mismo servicio. Ese es el objetivo: la alternativa es una copia de los datos de emparejamiento en la herramienta interna, es decir, un segundo sitio del que filtrar y un segundo sitio que se queda obsoleto.
Proteger el endpoint público, sabiendo lo que vale esa protección
Una consulta de código por dirección de correo es una consulta que cualquiera puede intentar. Los formularios se renderizan tras una verificación anti-bot, lo que corta el rascado casual.
Conviene ser claro sobre lo que eso compra. Una verificación delante de un formulario no protege un endpoint: el endpoint sigue siendo invocable directamente. Si tiene que ser público, las opciones honestas son limitar la frecuencia por dirección, exigir sesión autenticada para la consulta por dirección, o aceptar que quien conozca una dirección de cuenta puede leer su código actual y diseñar el resto del sistema con eso en mente.
Verificar en el pago, no en la entrega
La victoria más sutil no tiene nada que ver con los códigos. Cuando un pedido pasa a proceso, comprueba que realmente se pueda producir un código para su cuenta:
if ($new_status !== 'processing') return;
if ($order->get_meta('_billing_generate_psn') == 'yes') return; // la cuenta la pone la tienda
$psn_id = $order->get_meta('_billing_psn_id');
if (empty($psn_id)) throw new Exception('PSN ID is empty');
$code = $twoFaService->get2faCodeByEmail($psn_id);
if (empty($code['password'])) throw new Exception('2FA password is empty');
Sin esto, un pedido inviable parece exactamente igual que uno viable hasta que un operador lo toma, que es después de haber cobrado al cliente. Con esto, el fallo se escribe como nota del pedido en el momento en que entra el pago.
Fíjate en lo que la comprobación no hace: no detiene el pedido. Anotar en vez de bloquear es el comportamiento correcto por defecto mientras aún no conoces la tasa de falsos positivos de una comprobación nueva. Bloquear pedidos con una comprobación que se equivoca el cinco por ciento de las veces cuesta más que el problema que resuelve.
La regla general
Una credencial permanente debe vivir en un sistema, y todo lo demás debe pedirle a ese sistema respuestas de vida corta. En cuanto un secreto se copia por comodidad, la seguridad de cada consumidor se convierte en la seguridad del conjunto. Lo que hay que repartir es la respuesta, válida treinta segundos, y no aquello que la produce.
Disponible para colaboración por contrato
Estoy disponible para colaborar por contrato. Si tiene una idea de proyecto interesante, reserve una llamada por Calendly.
Agenda una llamada de 30 min