Autenticación vía JWT en Express.js (parte 2)

11 de junio 2019ComentariosjavascriptDavid Poza SuárezComentarios

Continuamos con el artículo sobre JWT de la semana pasada, pero esta vez quería mostrar la misma funcionalidad que habíamos logrado pero sin usar el paquete passport-jwt, con el único objetivo de ver de forma aún más clara la lógica que sigue el uso de tokens.

JWT sin passport

Básicamente modificaremos el manejador para el login y el middleware que teníamos para comprobar la autenticación del usuario. El manejador para el registro seguirá siendo el mismo que teníamos. También continuaremos usando la librería jsonwebtoken para crear la firma y verificar la misma. Del mismo modo vamos a continuar usando bcrypt para almacenar las password como hash.

flujo de autenticación por token jwt

flujo de autenticación por token jwt

Login:

Se realiza la búsqueda del usuario el base de datos directamente en el método login (con passport llamábamos a passport.auth("local"...). En caso de no encontrarlo o de no coincidir la clave se lanzará un error 404 (no encontrado).

Si efectivamente tenemos que crear el token lo hacemos exactamente como en el ejemplo con passport, creando el objeto payload siguiendo el estándar JWT:

payload (base 64Url del JSON.stringify): contiene los privilegios o claims. El estándar define unos cuantos posibles:

  • sub: identificador del usuario
  • iat: timestamp del momento de creación.
  • exp: timestamp en segundos a partir del cual el token dejará de ser válido. Es importante, desde el punto de vista de la seguridad, generar tokens con caducidad.
login: (req, res, next) => {
    console.log("caso login");
    var params = req.body;
    var username = params.username;
    var password = params.password;
    User.findOne({username: username})
    .then(user=>{
        if(user === null || !bcrypt.compareSync(password, user.password))
            next(new error\_types.Error404("username or password not correct."));
        else{
            console.log("\*\*\* comienza generacion token\*\*\*\*\*");
            const payload = {
                sub: user.\_id,
                exp: Math.round(Date.now()/1000) + parseInt(process.env.JWT\_LIFETIME),
                username: user.username
            };
            const token = jwt.sign(JSON.stringify(payload), process.env.JWT\_SECRET, {algorithm: process.env.JWT\_ALGORITHM});
            res.json({ data: { token: token } });
        }
    })
    .catch(err=>next(err)) // error en DB
}

Middleware ensureAuthenticated:

Este middleware lo inyectamos antes de la peticiones que queramos proteger. Lo primero es comprobar que incluye la cabecera de authorization, que es una cadena con el contenido "Bearer " + token. Este punto lo realizaba passport-jwt por medio de ExtractJwt.fromAuthHeaderAsBearerToken().

A continuación incluiremos en el middleware la lógica correspondiente a passport.auth("jwt",...), para lo cual usaremos la librería jsonwebtoken (que de hecho es la misma que usa passport). Con la función verify estaremos obteniendo el payload del token y además detectando cualquier error en la validez de la firma o la caducidad, momento en que lanzaremos un error 401 (no autorizado).

En caso contrario, tendríamos un token totalmente válido, pero aún quedaría el paso más importante que es comprobar si pertenece a un usuario que existe (o que está activo, por ejemplo). Si no encontramos ese usuario estaríamos en el caso de un error 403 (prohibido).

El último paso es común al middleware que hicimos en la primera parte del artículo: Solo nos quedaría inyectar los datos de usuario en la petición actual.

ensureAuthenticated: (req,res,next)=>{
    if(!req.headers.authorization){
        return next(new error\_types.Error403("Missing Authorization header."));
    }
    let token = req.headers.authorization.split(" ")\[1\];
    jwt.verify(token, process.env.JWT\_SECRET, {algorithms: \[process.env.JWT\_ALGORITHM\]}, (err,payload)=>{
        if(err){//comprueba validez, caducidad, etc.
            return next(new error\_types.Error401(err.message));
        }
        else{
            User.findOne({\_id: payload.sub})
            .then(data=>{
                if (data === null) { //no existe el usuario
                    //podríamos registrar el usuario
                    return next(new error\_types.Error403("You are not allowed to access."));
                }
                /\*encontramos el usuario así que procedemos a devolverlo para
                inyectarlo en req.user de la petición en curso\*/
                else{
                    req.user = data;
                    next();
                }
            })
            .catch(err=>next(err)) //si hay un error en la consulta a db, lo devolvemos
        }
    });
}

Conclusión

Como podemos ver el uso de passport realmente está justificado si vamos a integrar una estrategia más compleja (Google, Facebook, etc) o bien queremos soportar varias a la vez, puesto que para implementar únicamente el uso de token JWT ya tenemos un código muy sencillo y fácil de entender.