En este post vamos a centrarnos en algunas de las posibilidades de implementación de autenticación en Node, concretamente hablaremos de lo que se conoce como Basic Auth y de JSON Web Token.
Basic Auth
Este sistema de autenticación es el más sencillo de todos, y a su vez no de lo más seguro, pero puede que para un sistema sencillo o alguna aplicación interna pueda servirnos (en plan....bueno voy a intentar que no entre todo el mundo pero si entras....pues bienvenido pásalo bien ;) ).
Vamos a configurar por ejemplo nuestra ruta con heros, la que está en el fichero ninjas.js para que nos solicite un usuario y contraseña.
Lo primero de todo como es habitual en Node, usaremos npm para instalar el módulo necesario
npm install basic-auth --save
A continuación lo que hacemos es requerir el módulo como siempre y lo siguiente en el middleware que queramos usamos lo que hemos requerido y le pasamos la request. Esto lo que hará será buscar datos de usuario y contraseña en ella.
//Con esto importamos el módulo
const auth = require('basic-auth');
Y en nuestro middleware
router.get('/heros', function(req, res, next) {
const userLogin = auth(req);
Ahora si lo probaramos esto no haría nada, tenemos que añadirle algunas cosas más.
Tenemos que hacer que nos solicite usuario y contraseña, para hacer esto tenemos que responder a nuestro cliente desde nuestro middleware con un valor espécifico en la cabecera y ya de paso para hacerlo bien le ponemos el código de estado pertinente (401 en este caso). Pero claro esto no es tan automático como podemos pensar, necesiteamos controlar si ya nos ha pasado usuario y contraseña en nuestra constante userLogin, con todo esto de momento nuestro código sería el siguiente
router.get('/heros', function(req, res, next) {
const userLogin = auth(req);
if (!userLogin) {
res.set('WWW-Authenticate', 'basic realm=Authorization Required');
res.send(401);
return;//Esto para que no continue la ejecución del middleware
}
res.json({ninja:"Somos un monton de ninjas autenticados" })
});
Si ahora lo probamos tendríamos esto
Como veis nos pide usuario y contraseña pero claro ahora mismo pongamos lo que pongamos nos dejará pasar, tenemos que comprobar el usuario y contraseña de alguna forma. En un principio esto lo ideal sería tener una base de datos, un fichero o similar para comprobar tanto usuario como contraseña, en nuestro caso no tenemos implementado nada de esto por lo que de momento lo que haremos será comprobarlo directamente en el código, pero tener en cuenta que en este punto lo que haríamos sería comprobar en algún lado que los datos son correctos
router.get('/heros', function(req, res, next) {
const userLogin = auth(req);
if (!userLogin) {
res.set('WWW-Authenticate', 'basic realm=Authorization Required');
res.send(401);
return;
}
//Aqui haríamos la comprobación en nuestras base de datos o similar
if (userLogin.name !== 'ninja' || userLogin.pass !== '1234' ){
res.set('WWW-Authenticate', 'basic realm=Authorization Required');
res.send(401);
return;
}
res.json({ninja:"Somos un montón de ninjas autenticados" })
});
Ahora ya si lo probamos solo funcionará si introducimos ese usuario y contraseña
Como veis es bastante simple, veamos ahora como podemos hacer esto un poco más elegante, no creo que sea lo mejor hacer este código en todos los middleware que necesitemos (bueno si es general para toda la aplicación solo necesitamos ponerlo en el app.js y crear un middleware que responda a todas las request, pero ese no creo que sea el escenario en general).
Vamos a crear nuestro propio módulo para gestionar esto, primero creamos una carpeta lib que sera la que tendrá las librerías/módulos que creemos o importemos y no lo hagamos con npm. Creamos un fichero auth.js y lo que haremos será exportar el callback al que respondería un middleware
'use strict';
const auth = require('basic-auth');
module.exports = (req, res, next) => {
const userLogin = auth(req);
if (!userLogin) {
res.set('WWW-Authenticate', 'basic realm=Authorization Required');
res.send(401);
return;
}
//Aqui haríamos la comprobación en nuestras base de datos o similar
if (userLogin.name !== 'ninja' || userLogin.pass !== '12345' ){
res.set('WWW-Authenticate', 'basic realm=Authorization Required');
res.send(401);
return;
}
next();//Para que continue con el siguiente middleware
};
Básicamente he puesto el mismo código que habíamos añadido antes, solo que al final le hemos puesto un next() para que continue con el siguiente middleware que coincida.
Ahora vamos a módificar nuestra ruta ninjas para que use esto
'use strict'
var express = require('express');
var router = express.Router();
const auth = require('../lib/auth.js');
/* GET home page. */
router.get('/heros', auth, function(req, res, next) {
res.json({ninja:"Somos un montón de ninjas autenticados" })
});
module.exports = router;
Hemos importado nuestro módulo y simplemente se lo hemos pasado como primer handler del middleware. A un middleware le podemos pasar todos los handlers que queramos y se irán ejecutando uno detrás de otro según vayan terminando.
Si ahora lo probáis debería funcionaros igual que antes, probar a cambiar la contraseña porque seguramente el navegador haya almacenado los datos que habéis introducido anteriormente.
Bueno en cuanto a este método de autenticación poco más merece que miremos, es bastante sencillo y pueden existir otro tipo de implementaciones pero este sería el funcionamiento típico.
JSON Web Token
Y, ¿qué es JSON Web Token?... básicamente es una forma de tener una sesión pero almacenandola en el cliente, lo que aligera un poco la gestión de esto por parte del backend y como su nombre indica lo hacemos en formato JSON.
El proceso sería el siguiente. El usuario se autentica con su usuario y contraseña, el cual se comprueba de alguna forma (BBDD, fichero, etc...), el backend una vez que confirma los datos le devuelve al usuario un token. A partir de entonces todas las peticiones HTTP del usuario deben ir acompañadas de este token(cabecera, queryString...). El token se almacena en el lado del cliente de alguna forma(sessionStorage o localStorage por ejemplo), lo que hace que no tenga que mantener esta información el backend, lo que convierte todo en bastante más escalable. El Token es una firma cifrada que el backend descifra y verifica al recibirla lo que permite confirmar la identidad del usuario.
Este tipo de autenticación nos permite utilizar el mismo tipo de autenticación para todos nuestros desarrollos (Web, Android, IOS,....) solo es necesario hacer la gestión pertinente del token. También nos provee de algo más de seguridad, ya que no usa cookies para mantener información de usuario se pueden evitar ciertos tipos de ataques que manipulen la sesión y además podemos hacer que el token expire cada cierto tiempo lo que nos provee de otra capa más de seguridad.
Vamos a hablar un poco del token que recibimos para que entendamos un poco que información lleva. Este de aqui será un token tipo
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibmluamEiLCJpYXQiOjE1MDY4NDk4NTEsImV4cCI6MTUwNzAyMjY1MX0.qVSS9iQAyIQzjlp4uCyiJUOGSgXMSbmZUAJ7hQncco4
Si os fijáis bien esta compuesto por tres strings separados por puntos, hablemos un poco de cada uno
1º Header
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Esto básicamente indica el tipo de token y el algoritmo usado para codificar, normalmente es HMAC SHA256. Descodificado sería algo similar a esto:
{
"alg": "HS256",
"typ": "JWT"
}
2º Payload/Data
eyJ1c2VyIjoibmluamEiLCJpYXQiOjE1MDY4NDk4NTEsImV4cCI6MTUwNzAyMjY1MX0
La segunda parte tiene datos extra que indicamos nosotros desde el backend que se guarden, ya sea para recomprobar en el backend o cualquier otra cosa. Este payload a parte de la información que queramos nosotros guardar, tiene lo que se conoce como claims, son atributos propios que definen el token(aqui más en detalle), tiene mínimo 2:
- iat: La fecha en la que se creó el token.
- exp: La fecha de expiración del token. Esto normalmente lo indicamos nosotros desde el backend.
Descodificado este tendría:
{
"user": "ninja",
"iat": 1506849851,
"exp": 1507022651
}
3º Firma/Signature
qVSS9iQAyIQzjlp4uCyiJUOGSgXMSbmZUAJ7hQncco4
Esta última parte es la que realmente comprobamos en el backend, esta compuesta de los 2 strings anteriores codificados en BASE64 junto con una clave secreta que indicamos nosotros en el backend, por lo que si lo que descodifica en este punto es igual que lo anterior da por sentado que es correcto.
Descodificado sería:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
Secret KEY
)
Secret KEY es la clave secreta que hemos usado para codificar. Podemos comprobar este token completo en la pagina oficial de JWT.io solo es necesario copiar el token completo en la parte donde pone ENCODED y poner la clave secreta "Secret KEY" en la zona donde pone VERIFY SIGNATURE.
Bueno ya hemos visto un poco de teoría sobre de que esta compuesto el token, ahora vamos a implantar JWT en Node para que veamos un ejemplo de como se hace implanta.
Lo primero de todo como siempre es instalar el módulo de jsonwebtoken con npm.
npm install jsonwebtoken --save
Una vez instalado, vamos a crearnos otra ruta para hacer el login (así repasamos un poco), para ello en la carpeta routes creamos el fichero login.js con este contenido
var express = require('express');
var router = express.Router();
const jwt = require('jsonwebtoken')
router.post('/', function(req, res, next) {
const user = req.body.user;
const pass = req.body.pass;
if (!user){
return res.status(401).json({sucess: false, error: 'Auth failed. We need a valid user'});
}
if (!pass){
return res.status(401).json({sucess: false, error: 'Auth failed. We need a valid pass'});
}
//Aqui lo suyo es que comprobaramos con una base de datos o fichero el usuario y contraseña
if (user !== 'ninja' || pass !== '1234'){
return res.status(401).json({sucess: false, error: 'Auth failed. Invalid user or password'});
}
//La clabe secreta que pone Secret KEY debería estar en un fichero de configuración o deberíamos usar variables de entorno para que no este puesta directamente
const token = jwt.sign({user: user},'Secret KEY',{expiresIn: "2 days"});
res.json({sucess: true, token: token});
});
module.exports = router;
Como siempre requerimos express e instanciamos router, como extra requerimos nuestro nuevo módulo jsonwebtoken
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken')
Hacemos que este nuevo middleware responda a peticiones post, necesario ya que mandaremos la información en el body, y extraemos el usuario y contraseña para comprobarlos(se podría hacer en el cliente que la contraseña se codificara de alguna forma para mayor seguridad pero eso esta fuera de lo que queremos ver con Node)
router.post('/', function(req, res, next) {
const user = req.body.user;
const pass = req.body.pass;
Ponemos comprobaciones típicas, por si el usuario o contraseña estan vacios y en nuestro caso que usuario y contraseña están bien, pero recordar que esto realmente se debería comprobar en una base de datos o similar, esto es solo para agilizar el desarrollo
if (!user){
return res.status(401).json({sucess: false, error: 'Auth failed. We need a valid user'});
}
if (!pass){
return res.status(401).json({sucess: false, error: 'Auth failed. We need a valid pass'});
}
if (user !== 'ninja' || pass !== '1234'){
return res.status(401).json({sucess: false, error: 'Auth failed. Invalid user or password'});
}
Ahora tenemos donde creamos el token
const token = jwt.sign({user: user},'Secret KEY',{expiresIn: "2 days"});
Le hemos puesto un payload/data en formato JSON(da menos problemas con este formato) que contiene el usuario. Luego le hemos indicado la clave secreta para componer el hash Secret KEY, **IMPORTANTE esto no debería estar así deberíamos ponerlo en un fichero de configuración o en las variables de entorno de nuestro backend, para extraerlo con process.env... **. Por último le hemos indicado el tiempo de expiración de nuestro token, esto tiene varios formatos podéis verlos en la página de npm sobre JSON Web token.
Por último le devolvemos al cliente el token que hemos generado para que lo use
res.json({sucess: true, token: token});
Ya tenemos nuestra nueva ruta creada, la indicamos en el app.js, importamos la nueva ruta, por ejemplo debajo de la de ninjas que creamos anteriormente
var ninjas = require('./routes/ninjas');
var login = require('./routes/login');
Y le indicamos a express que la use(debajo de la de ninjas por ejemplo)
app.use('/ninjas',ninjas);
app.use('/login',login);
Voilá ya lo tenemos ahora vamos a probarlo, lo mejor sería hacerlo con un software tipo POSTMAN o similar que nos permite personalizar como hacemos las peticiones HTTP.
Como hemos configurado le pasamos en el body por post el usuario y contraseña.
Como veis tenemos nuestro token jejejeje, relativamente sencillo ¿verdad? Si cambiamos la contraseña veréis como nos devuelve algún tipo de 401
Bueno ya tenemos el token.....pero nos falta algún sitio para usarlo, ¿no?...pues vamos a por ello.
Al igual que hicimos arriba con Basic Auth vamos a crearnos un módulo para gestionar la verificación del login. En la carpeta lib creamos el fichero authJWT.js y ponemos lo siguiente
const jwt = require('jsonwebtoken');
module.exports = function(req,res,next) {
const token = req.query.token;
if (token){
jwt.verify(token,'Secret KEY',(err,decoded) => {
if (err){
console.log(decoded)
return res.status(401).json({success: false, error: 'Failed to authenticate token'});
} else {
console.log('Decoded',decoded);
next();
}
});
}else{
return res.status(403).json({success: false, error: 'No token provided'})
}
}
El código es bastante simple, recibimos el token por queryString, y lo verificamos con jwt.verify pasandole también la clave privada que hemos usado y una vez comprobado saltaría el callback con el error o con el payload descodificado. Recordar clave secreta aqui kaka, tiene que estar en fichero de configuración o en variable de entorno
jwt.verify(token,'Secret KEY',(err,decoded) => {
Le he puesto un console.log para que veáis lo que descodifica si todo va bien o da error.
Por último tenemos que hacer que algún middleware lo use de alguna manera. En este caso vamos a cambiar el que usamos con Basic Auth y usamos el nuevo con JWT. Nos vamos a ninjas.js y ponemos lo siguiente
'use strict'
var express = require('express');
var router = express.Router();
//const auth = require('../lib/auth.js');
const authJWT = require('../lib/authJWT.js');
router.get('/heros', authJWT, function(req, res, next) {
res.json({ninja:"Somos un monton de ninjas autenticados" })
});
module.exports = router;
Comentamos lo que pertenecia a basic-auth y hacemos lo mismo que hicimos antes pero con JWT y ya lo tendríamos, vamos a probarlo para ello el token que nos ha devuelto antes se lo pasamos como queryString
http://localhost:3000/ninjas/heros?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibmluamEiLCJpYXQiOjE1MDY4NTUzNTAsImV4cCI6MTUwNzAyODE1MH0.RS-ow1UqTz66l5X26rpKw5qB-1gX31t8_ycNgIkdUuE
Y si todo va bien veremos esto
y si por algún motivo el token no es correcto
Bueno como hemos visto usar JSON web Token es relativamente sencillo, en este caso no hemos usado base de datos pero lo ideal seria tener los usuarios y contraseñas almacenados en una base de datos, con la contraseña codificada con algún tipo de hash por supuesto, y hacer las comprobaciones con ellos, no tenerlos en nuestro código puestos a mano.
Como extra comentaros que casi todos los métodos de autenticación con token (login con terceros como Google, Facebook, OAuth, Azure o AWS por ejemplo) suelen funcionar de forma similar, es decir un token que almacenamos y pasamos en cada petición normalmente en el header con un nombre tipo x-access-token, authorization o similar.
Y hasta aquí la parte de autenticación en el siguiente post y el último por el momento de esta serie de Node (Lo siguiente será mezclar con MongoDB) hablaremos de Promesas que aunque no es propio de NodeJS, es más de JS creo que es útil que las repasemos y también veremos como poner nuestro proceso de Node en cluster para aumentar el rendimiento con un gran númeo de peticiones. Luego empezaré una linea nueva de post donde hablaremos de MongoDB y ya de paso lo integraremos con Node.
Nos veeeemossss un abrazooorrrrrr






