Herel Odin

Desarrollador Web

¿Como configurar tokens y policies con sailsJs 1.0.x en mongodb y redis ?

by herel Last updated octubre 31, 2020 Comments

Es tiempo de usar policies, ya vimos como registrar a los usuarios y como iniciar sesion ahora debemos guardar nuestro token ya sea en el localstorage de nuestro navegador, como cookie, session o en el storage de nuestro aplicacion de ios/android. En otro post hablaremos sobre el tema.

En este ejemplo vamos actualizar la información el usuario como firstName y lastName.

1.- Diagrama de flujo


2.- Policies

Vamos a crear el archivo /api/policies/Auth.js donde vamos a validar que el token viene en los headers de la petición con el atributo Authorization decodificar el token y verificar que exista en redis https://github.com/herel/api-login/blob/master/api/policies/Auth.js

module.exports = function(req, res, next) {
	var token = req.headers.authorization;
	// si no viene el token mandamos el error 401
	if(!token)
		return res.send(401,{ error : true, message : "token is required", status : 401 })
	TokenService
		.decode(token)
		.then(function(decoded) {
			...
		})
		.catch(function(err){
			//ocurrio un error al decodificar o alguien genero un token con el key incorrecto.
			return res.send(500,{ error : true, message : "Internal server error", status : 500 });
		});
}

module.exports = function(req, res, next) { var token = req.headers.authorization; // si no viene el token mandamos el error 401 if(!token) return res.send(401,{ error : true, message : "token is required", status : 401 }) TokenService .decode(token) .then(function(decoded) { ... }) .catch(function(err){ //ocurrio un error al decodificar o alguien genero un token con el key incorrecto. return res.send(500,{ error : true, message : "Internal server error", status : 500 }); }); }

En este pequeño codigo obtenemos de los headers el token en caso de que el token no este en los header mostramos el error correspondiente, recordemos en el tutorial para iniciar sesion creamos un servicio llamado token service que se encarga de generar el token y guardarlo en redis con el KEY “SESSION::USERID” ahora es tiempo de decodificar el token y ser su contenido para eso creamos en tokenService la funcion decode. https://github.com/herel/api-login/blob/master/api/services/TokenService.js

decode: function(token) {
		return new Promise(function(resolve, reject) {
			jwt.verify(token, process.env.TOKEN_KEY, function(err, decoded) {
				if (err)
					return reject({
						error: true,
						message: "Ocurrio un error al decodificar el token ",
						status: 500
					});
				return resolve(decoded);
			});
		});
	}

decode: function(token) { return new Promise(function(resolve, reject) { jwt.verify(token, process.env.TOKEN_KEY, function(err, decoded) { if (err) return reject({ error: true, message: "Ocurrio un error al decodificar el token ", status: 500 }); return resolve(decoded); }); }); }

¡Bien! ahora debemos obtener el key que se encuentra en la memoria ram si es que aun no expira, si existe y ambos tiene el mismo userId significa que el token es valido  y podemos pasar a nuestro controller para terminar el proceso de actualizar el nombre y apellidos.

Si queremos que el token sea unico es decir si inician sesion en otro dispositivo y  se cierre la sesion actual debemos validar que el create y expire sean iguales  ya que al crear un token nuevo estos valores se actualizan.

https://github.com/herel/api-login/blob/master/api/policies/Auth.js

module.exports = function(req, res, next) {
	var token = req.headers.authorization;
	// si no viene el token mandamos el error 401
	if(!token)
		return res.send(401,{ error : true, message : "token is required", status : 401 })
	TokenService
		.decode(token)
		.then(function(decoded) {
			RedisService.get("TOKEN::" + decoded.userId)
				.then(function(result) {
					if(!result){
						//el token no existe en redis ya expiro
						//lo idea es mandar un error 403 para que en nuestra app se cierre en automatico cada vez que un en point
						// conteste el status 403
						return res.send(403,{ error : true, message : "La sesion ya expirto", status : 403 });
					}
					//si los datos del token son iguales a los que estan en redis entonces
					// el token es valido
					// si es diferente entonces el usuario inicio sesion en otro dispotivo y se anulo el token actual
					if( result.create == decoded.create &&
						result.expire == decoded.expire &&
						decoded.userId == result.userId){
						//aqui esta la magia a nuestro req le agregamos el userId que hizo la petición ya mostrare en el controller
						// como utilizar esta variable
						req.userId = result.userId.toString();
						//mandamos la peticion a nuestro controller
						return next();
					}else
						return res.send(403,{ error : true, message : "La sesion ya expirto", status : 403 });
				}).catch(function(err){
					return res.send(500,{ error : true, message : "Internal server error", status : 500 });
				});
		})
		.catch(function(err){
			//ocurrio un error al decodificar o alguien genero un token con el key incorrecto.
			return res.send(500,{ error : true, message : "Internal server error", status : 500 });
		});
}

module.exports = function(req, res, next) { var token = req.headers.authorization; // si no viene el token mandamos el error 401 if(!token) return res.send(401,{ error : true, message : "token is required", status : 401 }) TokenService .decode(token) .then(function(decoded) { RedisService.get("TOKEN::" + decoded.userId) .then(function(result) { if(!result){ //el token no existe en redis ya expiro //lo idea es mandar un error 403 para que en nuestra app se cierre en automatico cada vez que un en point // conteste el status 403 return res.send(403,{ error : true, message : "La sesion ya expirto", status : 403 }); } //si los datos del token son iguales a los que estan en redis entonces // el token es valido // si es diferente entonces el usuario inicio sesion en otro dispotivo y se anulo el token actual if( result.create == decoded.create && result.expire == decoded.expire && decoded.userId == result.userId){ //aqui esta la magia a nuestro req le agregamos el userId que hizo la petición ya mostrare en el controller // como utilizar esta variable req.userId = result.userId.toString(); //mandamos la peticion a nuestro controller return next(); }else return res.send(403,{ error : true, message : "La sesion ya expirto", status : 403 }); }).catch(function(err){ return res.send(500,{ error : true, message : "Internal server error", status : 500 }); }); }) .catch(function(err){ //ocurrio un error al decodificar o alguien genero un token con el key incorrecto. return res.send(500,{ error : true, message : "Internal server error", status : 500 }); }); }

¡Aqui esta la magia! si todo el proceso es correcto noten como al requests le agregamos el userId con el id de mongodb para poder utilizar el atributo en el controller y saber que usuario esta haciendo la petición.

3.- Configurando nuestro controller

Vamos a crear el archivo userController se que encargara de procesar todas las acciones que tengan que ver con la collection user, como por ejemplo actualizar un usuario, eliminar un usuario, actualizar foto de perfil, actualizar contraseña etc…

creamos el archivo /api/controllers/UserController y creamos la funcion update.

module.exports = {
	update : function(req,res){
		var params = req.allParams();
		var userId = req.userId;
 
		// validamos que los parametros a actualizar sean validos
		if(!params.firstName || params.firstName.length <= 3)
			return res.send(401, { error : true, message : "El nombre es obligatorio o es muy corto", status : 401 });
		if(!params.lastName || params.lastName.length <= 3)
			return res.send(401, { error : true, message : "El apellido es obligatorio o es muy corto", status : 401 });
		...
	}
}

module.exports = { update : function(req,res){ var params = req.allParams(); var userId = req.userId; // validamos que los parametros a actualizar sean validos if(!params.firstName || params.firstName.length <= 3) return res.send(401, { error : true, message : "El nombre es obligatorio o es muy corto", status : 401 }); if(!params.lastName || params.lastName.length <= 3) return res.send(401, { error : true, message : "El apellido es obligatorio o es muy corto", status : 401 }); ... } }

Validamos los campos necesarios en este caso solo requerimos validar el firstName y lastName.

Como recordaremos creamos un servicio llamado UserService ahora vamos a crear una funcion llamada  update donde vamos actualizar de forma nativa los campos de firstName y lastName.

update : function(userId,params){
		return new Promise(function(resolve, reject) {
			User.native(function(err, collection) {
				if(err)
					return reject({ error : true, message : "Internal server error", status : 500 });
				var query = { $set : { }};
				if(params.firstName)
					query.$set.firstName = params.firstName;
				if(params.lastName)
					query.$set.lastName  = params.lastName;
 
				collection.findAndModify({
					_id : ObjectId(userId.toString()),
					active : true
				},{
					// sort
				},query,{
					new : true,
					fields : {
						password : 0
					}
				},function(err,result){
					if(err)
						return reject({ error : true, message : "Internal server error", status : 500 });
					if(result && result.value)
						return resolve(result.value);
					//el usuario no existe
					return resolve(false);
				});
			});	
		});
	}

update : function(userId,params){ return new Promise(function(resolve, reject) { User.native(function(err, collection) { if(err) return reject({ error : true, message : "Internal server error", status : 500 }); var query = { $set : { }}; if(params.firstName) query.$set.firstName = params.firstName; if(params.lastName) query.$set.lastName = params.lastName; collection.findAndModify({ _id : ObjectId(userId.toString()), active : true },{ // sort },query,{ new : true, fields : { password : 0 } },function(err,result){ if(err) return reject({ error : true, message : "Internal server error", status : 500 }); if(result && result.value) return resolve(result.value); //el usuario no existe return resolve(false); }); }); }); }

Como ya tenemos el userId que esta haciendo la peticion solo actualizamos los valores correspondientes y si todo sale bien regresarmos el objeto actualizado pero recuerda ocultar el password.

Ahora es tiempo de utilzar el servicio en nuestro controller https://github.com/herel/api-login/blob/master/api/controllers/UserController.js

module.exports = {
	update : function(req,res){
		var params = req.allParams();
		var userId = req.userId;
 
		// validamos que los parametros a actualizar sean validos
		if(!params.firstName || params.firstName.length <= 3)
			return res.send(401, { error : true, message : "El nombre es obligatorio o es muy corto", status : 401 });
		if(!params.lastName || params.lastName.length <= 3)
			return res.send(401, { error : true, message : "El apellido es obligatorio o es muy corto", status : 401 });
		async.waterfall([
			function updateUser(cb){
				UserService.update(userId,params)
					.then(function($user){
						if(!$user)
							return cb({ error : true, message : "El usuario no existe", status : 404 });
						return cb(null,$user);
					}).catch(cb);
			}
		],function done(err,result){
			if (err && err.status) return res.send(err.status, err);
			else if (err)
				return res.send(500, {
					error: true,
					message: "Internal server error",
					status: 500
				});
			return res.json(result);
		});
	}
}

module.exports = { update : function(req,res){ var params = req.allParams(); var userId = req.userId; // validamos que los parametros a actualizar sean validos if(!params.firstName || params.firstName.length <= 3) return res.send(401, { error : true, message : "El nombre es obligatorio o es muy corto", status : 401 }); if(!params.lastName || params.lastName.length <= 3) return res.send(401, { error : true, message : "El apellido es obligatorio o es muy corto", status : 401 }); async.waterfall([ function updateUser(cb){ UserService.update(userId,params) .then(function($user){ if(!$user) return cb({ error : true, message : "El usuario no existe", status : 404 }); return cb(null,$user); }).catch(cb); } ],function done(err,result){ if (err && err.status) return res.send(err.status, err); else if (err) return res.send(500, { error: true, message: "Internal server error", status: 500 }); return res.json(result); }); } }

Si el token fuera valido pero el usuario no esta activo por que se elimino o infringe reglas de nuestra app y colocamos el active de true a false, entonces mandamos el error 404 donde indicamos que el usuario no existe.

¡Genial ! nuestro usuario se actualizo con existo y el mismo proceso se repite para miles de peticiones con diferente usuario pero solo actualizando el usuario correspondiente es imposible actualizar a otro usuario si no se tiene el token correcto.

Por ultimo contestamos la petición con el usuario actualizado para poder utilizar la data en nuestro sitio web, app  y mostrar los cambios realizados.

4.- Configurar policie

Debemos ir al archivo config/policies.js e indicar que todas las funciones del userController van a pasar antes por el archivo Auth.js lo hacemos de la siguiente  forma https://github.com/herel/api-login/blob/master/config/policies.js

module.exports.policies = {
 
  '*': ['Auth'],
  AccountController : true,
  UserController 	: ["Auth"]
};

module.exports.policies = { '*': ['Auth'], AccountController : true, UserController : ["Auth"] };

6.- Configurar endpoint

Como vimos en el tutorial anterior debemos configurar en nuestro archivo routes.js la ruta para actualizar los usuarios y lo hacemos asi https://github.com/herel/api-login/blob/master/config/routes.js

'POST /v1.0/account' : 'AccountController.create',
'POST /v1.0/account/login' : 'AccountController.login',
'PUT /v1.0/account'        : 'UserController.update',

'POST /v1.0/account' : 'AccountController.create', 'POST /v1.0/account/login' : 'AccountController.login', 'PUT /v1.0/account' : 'UserController.update',

Como pueden notar la petición es de tipo PUT y es la misma url para crear un usuario solo cambio el tipo de request

7.- Probando con postman

Para este paso debemos hacer login y copiar el token para utlizarlo en el enpoint para actualizar y en postman agregar el atributo Authorization

en caso de no mandar el token el mensaje seria el siguiente: 

Pero si todo sale bien obtenemos el siguiente resultado

Si hacemos login  y usamos el token anterior el resultado es el siguiente.

Si hacemos login nuevamente ahora el usaurio viene con los  campos actualizados

Gracias por leer y compartir este tutorial cuentame que te parecio y que podemos mejorar aqui algunos libros que me han ayudado a mejorar las buenas practicas a la hora de programar.

Te puede interesar:

Como crear un recordatorio con NODEJS y REDIS
¿Cómo programar una app en react native y redux para registrar usuarios a través de un api rest part...
¿Cómo programar actions en react native y redux para iniciar sesión a través de un rest api?
¿Comó crear sesiones con webtokens, sails y redis?
Posted in: MongoDb, NodeJs Tagged with: jsonwebtoken, login, mongodb, redis, sails, tokens
Copyright © 2021 Herel Odin