typeOrm
Security
HTTP
Authentication
APIs
Express
En el mundo del desarrollo web moderno, la autenticación y autorización de usuarios son aspectos cruciales para proteger las APIs y los datos sensibles. El acceso basado en tokens en una de las estrategias mas usadas, de entre ellas destacan los JSON Web Tokens (JWT), un estándar abierto y ligero que define un mecanismo compacto y autosuficiente para la transmisión segura de información entre partes como cliente y servidor.
Un JWT es un token que contiene información sobre la identidad de un usuario y la autorización para acceder a recursos específicos. Se compone de tres partes:
☝️ Si no sabes lo que es un token, te recomiendo esta lectura.
Encabezado (Header): Especifica el tipo de token, el algoritmo de firma y otra información relevante.
Carga útil (Payload): Contiene los datos sobre el usuario, como la identificación, nombre, roles y fecha de expiración del token.
Firma: Garantiza la integridad y autenticidad del token mediante una firma digital utilizando un algoritmo criptográfico y una clave secreta compartida entre el servidor y el cliente.
El esquema a implementar en este caso puede resumirse en la siguiente imagen
Inicio de sesión: El usuario ingresa sus credenciales (correo electrónico y contraseña) en el cliente.
Autenticación: El servidor valida las credenciales del usuario y, si son correctas, genera un JWT con la información del usuario y lo firma con su clave secreta.
Envío del token: El servidor envía el JWT al cliente como respuesta a la solicitud de inicio de sesión.
Almacenamiento del token: El cliente almacena el JWT de forma segura, generalmente en el almacenamiento local o en una cookie HTTP.
Solicitudes posteriores: En cada solicitud posterior a la API, el cliente incluye el JWT en el encabezado de autorización.
Validación del token: El servidor verifica la firma del JWT para asegurar su autenticidad e integridad. Si el token es válido, extrae la información del usuario de la carga útil y la utiliza para autorizar el acceso al recurso solicitado.
Instala estas 3 librerías que se encargarán de generar los tokens JWT:
1npm install express-jwt @types/express-jwt jsonwebtoken @types/jsonwebtoken --save
El segundo paso es crear una ruta API que pueda ser llamada por el cliente para generar un token (también conocido como login), esta ruta recibirá el email
y password
del body
y buscará cualquier usuario en la base de datos que coincida con esos dos valores.
Si encuentra el valor, generará un token llamando a la función jwt.sign
.
1//esta línea va en tu public_routes.ts 2router.post('/token', safe(createToken)); 3 4// esta función va en tu actions.ts 5import jwt from 'jsonwebtoken' 6 7export const createToken = async (req: Request, res: Response): Promise<Response> =>{ 8 9 if(!req.body.email) throw new Exception("Please specify an email on your request body", 400) 10 if(!req.body.password) throw new Exception("Please specify a password on your request body", 400) 11 12 const userRepo = await getRepository(Users) 13 14 // Necesitamos validar que existe un usuario con este email y contraseña en la BD 15 const user = await userRepo.findOne({ where: { email: req.body.email, password: req.body.password }}) 16 if(!user) throw new Exception("Invalid email or password", 401) 17 18 // esta es la línea más importante en esta función, se crea un token JWT 19 const token = jwt.sign({ user }, process.env.JWT_KEY as string); 20 21 // devolver al cliente el usuario y el token creado recientemente 22 return res.json({ user, token }); 23}
Ahora tenemos que añadir un middleware que buscará el token en el Encabezado de autorización de la petición. El middleware interceptará cada petición y ejecuta la función next
para avanzar solo si logra validar el token, en caso contrario retornará un error.
Añade estos dos middlewares dentro de ./src/app.js
que se encargarán de hacer cumplir el token.
1// ⬆ todo lo ANTERIOR es público 2import jwt, { Options } from 'express-jwt'; 3 4let opt: Options = { secret: process.env.JWT_KEY as string, algorithms: ["HS256"] } 5app.use(jwt(opt)) 6// ⬇ todo lo que esté POR DEBAJO es público 7app.use(((err: any, req: any, res: any, next: any) => { 8 if (err) console.error(err); 9 if (err.name === 'UnauthorizedError') { 10 return res.status(401).json({ status: err.message }); 11 } 12 next(); 13}))
Cualquier endpoint que se añada DEBAJO de estos middlewares será privado, por ejemplo:
1app.get('/public', (req, res) => { 2 res.json({ message: "Anyone can see me" }); 3}) 4 5// ⬆ anything ABOVE is public 6app.use(jwt(opt)) // ⬅ JWT Middleware 7// ⬇ anything BELOW is public 8 9app.get('/private', (req, res) => { 10 res.json({ message: "If you can se me, you are logged in" }); 11})
Hemos terminado, pero si sólo los usuarios registrados pueden llamar a nuestros endpoints privados, entonces necesitamos una forma de saber quién los está llamando, por ejemplo podemos usar req.user
de ahora en adelante, para identificar al usuario de la petición:
1export const getMe = async (req: Request, res: Response): Promise<Response> =>{ 2 3 const users = await getRepository(Users).find({ where: }); 4 // ⬇ no procedentes de la BD 5 return res.json(req.user); 6}
O podemos utilizar esa información y obtener más información del solicitante de la base de datos.
1export const getMe = async (req: Request, res: Response): Promise<Response> =>{ 2 3 4 // ⬇ not comming from the BD 5 return res.json(req.user); 6}
En el lado del front-end necesitamos dos pasos principales: Crear un nuevo token (también conocido como "login") y añadir el token a los headers cuando se obtenga cualquier otro endpoint privado.
Basándonos en los endpoints que construimos anteriormente, tenemos que hacer POST /token
con la información del nombre de usuario y la contraseña en el body de la petición.
1const login = async (username, password) => { 2 const resp = await fetch(`https://your_api.com/token`, { 3 method: "POST", 4 headers: { "Content-Type": "application/json" }, 5 body: JSON.stringify({ username, password }) 6 }) 7 8 if(!resp.ok) throw Error("There was a problem in the login request") 9 10 if(resp.status === 401){ 11 throw("Invalid credentials") 12 } 13 else if(resp.status === 400){ 14 throw ("Invalid email or password format") 15 } 16 const data = await resp.json() 17 // Guarda el token en la localStorage 18 // También deberías almacenar el usuario en la store utilizando la función setItem 19 localStorage.setItem("jwt-token", data.token); 20 21 return data 22}
Supongamos que estoy usando la aplicación de front-end y acabo de iniciar sesión, pero ahora quiero obtener algún endpoint privado o protegido:
1// Asumiendo que "/protected" es un endpoint privado 2const getMyTasks = async () => { 3 // Recupera el token desde la localStorage 4 const token = localStorage.getItem('jwt-token'); 5 6 const resp = await fetch(`https://your_api.com/protected`, { 7 method: 'GET', 8 headers: { 9 "Content-Type": "application/json", 10 'Authorization': 'Bearer ' + token // ⬅⬅⬅ authorization token 11 } 12 }); 13 14 if(!resp.ok) { 15 throw Error("There was a problem in the login request") 16 } else if(resp.status === 403) { 17 throw Error("Missing or invalid token"); 18 } else { 19 throw Error("Unknown error"); 20 } 21 22 const data = await resp.json(); 23 console.log("This is the data you requested", data); 24 return data 25}
¡Eso es todo! Como puedes ver, es muy sencillo integrar JWT en tu aplicación usando Express, solo cuatro pasos en el backend y dos pasos en el frontend. Ante cualquier duda puedes contactarme en Twitter @alesanchezr o utilizar el canal #public-support
en la comunidad Slack de 4Geeks Academy.