Autonomía digital y tecnológica

Código e ideas para una internet distribuida

Sistema seguro de registro y conexión de usuarios en PHP: password_hash, password_verify, cookie de sesión y el concepto de pepper

Imago voragine.net

La mayoría de las veces que se necesita un sistema de usuarios se usa el del CMS o el framework que se esté usando para desarrollar. Para las pocas veces que no sea el caso, o para quien quiera aprender cómo funciona un sistema de registro e inicio de sesión de usuarios a más bajo nivel, aquí algunas pistas de cómo construir uno de manera segura usando PHP.

En este caso lo voy a hacer usando las funciones password_hash y password_verify.

Para qué sirve password_hash y password_verify

La idea es la siguiente: un usuario utiliza un formulario para registrarse en el sitio web. Se almacena su nombre de usuario y su contraseña en base de datos. Para ello se usa password_hash. password_hash genera un hash, una secuencia de caracteres según un algoritmo de cifrado, a partir de la contraseña que se le suministre. password_hash genera cada vez una secuencia distinta aun a partir de la misma contraseña. Esta idea es importante.

<?php
// recogemos datos enviados desde el formulario de registro
$u = filter_var($_POST['username'],FILTER_SANITIZE_STRING);
$p = filter_var($_POST['password'],FILTER_SANITIZE_STRING);
// generamos el hash a partir de la contraseña enviada desde el formulario
$p_hashed = password_hash($p, PASSWORD_BCRYPT);
// almacenamos la informa del usuario en base de datos
// la siguiente función es solo un ejemplo
add_user_to_database($username, $p_hashed);
?>

Para asegurarse de alojar correctamente el hash en base de datos hay que hacerlo en un registro CHAR(60). Si la extensión es menor, el hash se almacenará incompleto.

Una vez registrado ese usuario utiliza otro formulario para iniciar sesión con su nombre de usuario y su contraseña. Para verificar la contraseña se usa password_verify. password_verify permite comprobar autenticidad de un hash con el password que se le suministre y así decidir que el password es válido o no. password_verify es capaz de encontrar la correspondencia entre cualquiera de los múltiples hash que puede generar password_hash (recuerdo que cada vez que se ejecuta, aún a partir de la misma contraseña, genera uno diferente) con la contraseña que se le suministre. Esta el la magia: la relación no es única.

<?php
// recogemos datos enviados desde el formulario de inicio de sesión
$u = filter_var($_POST['username'],FILTER_SANITIZE_STRING);
$p = filter_var($_POST['password'],FILTER_SANITIZE_STRING);
// recuperamos el hash del usuario almacenado en base de datos
$p_hashed = get_pwd_from_db($u);
// comprobamos si la contraseña enviada desde el formulario se corresponde con el hash alojado
if (password_verify($p, $p_hashed)) {
    // contraseña correcta
}
else {
    // contraseña incorrecta
}
?>

Qué es un pepper

Cuando se trata de crear un sistema seguro en definitiva la idea es que para encontrar la contraseña haya que invertir mucho tiempo y energía. Si el tiempo y la energía necesarios son suficientemente elevados, el sistema es en la práctica seguro. Este grado adicional de complejidad lo aporta el concepto de pepper, haciendo que un sistema basado en password_hash y password_verify sea difícil de craquear. Un pepper es un autentificador (token) secreto que sirve para hacer prácticamente imposible que tenga éxito un ataque por fuerza bruta.

El pepper hay que guardarlo en un lugar seguro, claro. Se recomienda no guardarlo en la base de datos que aloja los hashs porque vale justamente para ponerle las cosas difíciles a un atacante que ha conseguido acceso a la base de datos. En este caso, aún teniendo el hash contenido en la base de datos, no tendrá el pepper. El pepper se puede alojar en un archivo de configuración.

Si al código anterior que permitía registrar un usuario le añadimos la capa pepper quedaría algo como lo que sigue. Supongamos que el pepper lo alojamos en el archivo de configuración conf.php y que lo rescatamos con la función get_pepper():

<?php
$pepper = get_pepper();
// recogemos datos enviados desde el formulario de registro
$u = filter_var($_POST['username'],FILTER_SANITIZE_STRING);
$p = filter_var($_POST['password'],FILTER_SANITIZE_STRING);
// aplicamos el pepper a la contraseña enviada desde el formulario
$p_peppered = hash_hmac("sha256", $p, $pepper);
// generamos el hash a partir de la contraseña enviada desde el formulario
$p_hashed = password_hash($p_peppered, PASSWORD_BCRYPT);
// almacenamos la informa del usuario en base de datos
// la siguiente función es solo un ejemplo
add_user_to_database($username, $p_hashed);
?>

Y ahora al que permitía darle acceso a la web:

<?php
$pepper = get_pepper();
// recogemos datos enviados desde el formulario de inicio de sesión
$u = filter_var($_POST['username'],FILTER_SANITIZE_STRING);
$p = filter_var($_POST['password'],FILTER_SANITIZE_STRING);
// aplicamos el pepper a la contraseña enviada desde el formulario
$p_peppered = hash_hmac("sha256", $p, $pepper);
// recuperamos el hash del usuario almacenado en base de datos
$p_hashed = get_pwd_from_db($u);
// comprobamos si la contraseña enviada desde el formulario se corresponde con el hash alojado
if (password_verify($p_peppered, $p_hashed)) {
    // contraseña correcta
}
else {
    // contraseña incorrecta
}
?>

Mantener al usuario conectado durante toda una sesión usando cookies

Una vez que el usuario está registrado y ha iniciado sesión conviene que el sitio web no le pida introducir su contraseña conforme va navegando por él, con cada cambio de página web. Para mantener al usuario conectado se puede usar una cookie.

De nuevo, al código anterior le vamos a añadir esta nueva capa de la cookie, a la que vamos a llamar «Access»:

<?php
$pepper = get_pepper();
// recogemos datos enviados desde el formulario de inicio de sesión
$u = filter_var($_POST['username'],FILTER_SANITIZE_STRING);
$p = filter_var($_POST['pass'],FILTER_SANITIZE_STRING);
// aplicamos el pepper a la contraseña enviada desde el formulario
$p_peppered = hash_hmac("sha256", $p, $pepper);
// recuperamos el hash del usuario almacenado en base de datos
$p_hashed = get_pwd_from_db($u);

// comprobamos si el usuario ya ha iniciado sesión (si hay una cookie válida y activa)
if ( isset($_COOKIE['Access']) ) {
    if ( $_COOKIE['Access'] == md5($ppepper.$cookie_key) ) {
        // sesión abierta
    }
    else {
        // el usuario no está conectado.
        // mostrar el formulario de inicio de sesión
    }
}
// comprobamos si la contraseña enviada desde el formulario se corresponde con el hash alojado
elseif ( password_verify($p_pepper,$p_hash) ) {
    // si se completa un inicio de sesión, guardar la cookie "Access" para mantener la sesión abierta (le damos una duración de 1 semana a la sesión: 60*60*24*7 segundos)
    setcookie('Access', md5($ppepper.$cookie_key),time()+60*60*24*7);
    // Recargamos la página para evitar reenvíos del formulario de inicio de sesión
    header("Location: ?login=success");
    exit();
}
else {
    // el usuario no está conectado.
    // mostrar el formulario de inicio de sesión
}
?>

A la hora de alojar una cookie en el navegador del usuario es conveniente tomar algunas medidas de seguridad.

11 comentarios

    • Por German

    Muy buen aporte con password_hash, ya lo implemente en mi pagina, pero como dicen no hay seguridad al 100%, una pregunta? es la mejor técnica actualmente o hay otra mejor que esta? hablando para php 7.3

    1. Hola German,

      efectivamente no hay sistema cien por cien seguro. De lo que yo he podido leer, en PHP es de lo más seguro. Pero tampoco soy yo un experto en seguridad.

  1. Incredible

    • Por Borders

    Cheese

    • Por Jesús •

    Hola Amigos:

    Talvez ustedes puedan ayudarme, para empezar soy novato en esto. Estoy validando el campo password limitando los caracteres mínimo 4 y máximo 12. En mi base de datos, lo he dejado en 255, luego de esto $password = password_hash($password, PASSWORD_DEFAULT); el mensaje de error que recibo es que el código es muy largo y no puedo concluir el proceso de registro. Cuál sería la solución?. Gracias de antemano.

      • Por Andrés Palazón •

      Hola amigo gracias por esta explicación, hay algo que no me queda claro de la capa pepper, en el conf.php , ¿qué colocarías en el archivo?

  2. front-end

    • Por Andrés Palazón •

    Hola amigo gracias por esta explicación, hay algo que no me queda claro de la capa pepper, en el conf.php , ¿qué colocarías en el archivo?

    • Por Daniele

    Hola,

    creo que no este bien validar la sesion con cookies.
    Con un random generator, si encuentro el pepper de otro puedo robarle session.

    La session se tiene que validar con una $_SESSION vars,
    no con un $_COOKIE

    saludos

  3. back-end

    • Por Lon •

    Hola, muchas gracias por el aporte. Me surgen algunas dudas, que te agradecería si pudieras aclararlas. En el caso de encriptar una clave con un pepper, éste lo hemos tenemos que poner dentro de un archivo, pero podrías aclarar cómo y dónde ponerlo, y cómo recuperarlo?
    Y otra pregunta, en el caso de que ese pepper, por algún motivo, deba modificarse por motivos de seguridad, u otro motivo, qué pasa con los hashes ya guardados en la base de datos de los usuarios? ya no podrían recuperarse? En ese caso, tendrían todos los usuarios que cambiar su clave?
    Saludos y gracias de antemano.

×

Responder al comentario de Clothing

*
*

 

No hay trackbacks