Creación de un sistema de inicio de sesión exclusivo para el laboratorio
Hasta ahora hemos aprendido a crear páginas con HTML, configurar un servidor con Express, almacenar muestras en una base de datos y gestionar sesiones mediante cookies. Al llegar a este punto, surge una pregunta natural:
"¿Podemos restringir el uso de esta aplicación web únicamente al personal de nuestro laboratorio?"
Si el sistema de gestión de muestras contiene datos experimentales sensibles, no debe ser accesible para cualquier persona. Quizás desee compartir los resultados solo con un grupo de investigación colaborador o mostrar materiales de presentación de investigación exclusivamente a revisores que hayan iniciado sesión.
Para ello, se requiere un sistema de inicio de sesión.
De las cookies al inicio de sesión
En el tema de auth-cookies aprendimos sobre cookies y sesiones. La estructura permitía al servidor recordar "este navegador es la persona que inició sesión anteriormente".
Sin embargo, faltaba un elemento clave: ¿cómo verificar quién ha iniciado sesión? El proceso de recibir un nombre de usuario y una contraseña, compararlos con los almacenados en la base de datos y, si coinciden, crear una sesión. Si implementamos este proceso de autenticación directamente, el código se vuelve complejo y es propenso a errores de seguridad.
La herramienta que se encarga de este complejo proceso de autenticación es Passport.js.
Passport.js: middleware especializado en autenticación
Passport.js es un middleware de autenticación para Express. Es una herramienta diseñada específicamente para gestionar la autenticación.
npm install passportnpm install passport-localEl concepto clave es la estrategia (Strategy). Cada método de autenticación tiene su propio paquete de estrategia:
| Estrategia | Paquete | Método de autenticación |
|---|---|---|
| Local | passport-local | Correo electrónico + contraseña |
passport-google-oauth20 | Cuenta de Google | |
| GitHub | passport-github2 | Cuenta de GitHub |
| JWT | passport-jwt | Token web JSON (JWT) |
Actualmente, Passport.js tiene registradas más de 300 estrategias. Solo necesitas instalar la estrategia correspondiente al método de autenticación que necesites.
Configuración inicial de Passport: Integración con Express
Esta es la configuración básica para conectar Passport con Express:
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(session({
secret: "lab-secret-key",
resave: false,
saveUninitialized: false,
}));
app.use(passport.initialize());
app.use(passport.session());passport.initialize() — Registra el pasaporte en Express.
passport.session() — Restaura la información del usuario almacenada en la sesión en cada solicitud.
Se utiliza junto con lo aprendido en el tema anterior sobre auth-cookies express-session. Si la sesión es la "memoria", Passport determina "a quién se debe recordar".
serializeUser y deserializeUser
Cuando un usuario inicia sesión, se debe almacenar su información en la sesión. Sin embargo, incluir el objeto de usuario completo en la sesión puede ser ineficiente. Passport resuelve esto mediante el patrón de serialización/deserialización:
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
// Buscar usuario por ID en la base de datos
db.query("SELECT * FROM users WHERE id = ?", [id], function(err, rows) {
done(err, rows[0]);
});
});serializeUser — Se ejecuta al iniciar sesión correctamente. Determina qué datos se guardarán en la sesión. Normalmente, solo se guarda user.id.
deserializeUser — Se ejecuta en cada solicitud. Busca al usuario en la base de datos utilizando el ID guardado y lo almacena en req.user.
Como analogía, es similar a registrar solo el número de socio en una tarjeta de biblioteca (serializar) y, posteriormente, consultar la información del socio utilizando ese número (deserializar).
Autenticación local: correo electrónico + contraseña
Es el método de inicio de sesión más básico. Cuando el usuario ingresa su correo electrónico y contraseña, se verifica en la base de datos y se procesa el inicio de sesión.
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const bcrypt = require("bcrypt");
passport.use(new LocalStrategy(
{ usernameField: "email" },
function(email, password, done) {
db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {
if (err) return done(err);
if (rows.length === 0) return done(null, false, { message: "Correo electrónico no registrado" });
const user = rows[0];
const isMatch = bcrypt.compareSync(password, user.password_hash);
if (!isMatch) return done(null, false, { message: "Contraseña incorrecta" });
return done(null, user);
});
}
));done(null, user) — Autenticación exitosa. Passport pasa este usuario a la función serializeUser.
done(null, false) — Autenticación fallida. No es un error, pero se deniega el inicio de sesión.
done(err) — Error del servidor (por ejemplo, fallo en la conexión a la base de datos).
Lo que hace Passport:
- Transfiere el correo electrónico y la contraseña recibidos desde el formulario de inicio de sesión a la estrategia correspondiente.
- En caso de autenticación exitosa,
serializeUser→ guarda el ID del usuario en la sesión. - Para cada solicitud posterior,
deserializeUser→ permite acceder a la información del usuario actualmente conectado mediantereq.user.
Se trata del mismo patrón que el middleware de Express que vimos antes (app.use()). Si se integra Passport como middleware, es posible verificar al usuario conectado en las rutas utilizando req.user.
Registro: registro de un nuevo investigador
Para poder iniciar sesión, primero es necesario registrarse. El flujo completo es:
Usuario → Formulario de registro (ingresar correo y contraseña)
→ Servidor: Verificar duplicidad del correo electrónico
→ Servidor: Procesar hash de la contraseña (bcrypt)
→ Servidor: Guardar la contraseña con hash en la base de datos
→ Inicio de sesión posibleSi se implementa como una ruta de Express:
const bcrypt = require("bcrypt");
app.post("/register", function(req, res) {
const { email, password, name } = req.body;
// 1. Verificar duplicidad del correo electrónico
db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {
if (rows.length > 0) {
return res.status(400).send("El correo electrónico ya está registrado");
}
// 2. Procesar hash de la contraseña
const hashedPassword = bcrypt.hashSync(password, 10);
// 3. Guardar en la base de datos
db.query(
"INSERT INTO users (email, password_hash, name, role) VALUES (?, ?, ?, ?)",
[email, hashedPassword, name, "member"],
function(err) {
if (err) return res.status(500).send("Error de registro");
res.redirect("/login");
}
);
});
});Las contraseñas nunca se almacenan en formato de texto sin formato. Se convierten mediante funciones hash, como bcrypt, y luego se almacenan.
const bcrypt = require("bcrypt");
// Al registrarse: texto plano → hash
const hashedPassword = bcrypt.hashSync("myLabPassword", 10);
// → Guardar el valor hash como "$2b$10$X7YKz..."
// Al iniciar sesión: comparar el hash de la entrada con el hash almacenado
const isMatch = bcrypt.compareSync("myLabPassword", hashedPassword);
// → true o falsebcrypt.hashSync(password, 10) — 10 es el número de rondas de sal (iteraciones del hash). Cuanto mayor sea el número, más seguro, pero también más lento. El valor 10 es la recomendación habitual.
Esto es similar a la seguridad en un laboratorio. Si el código de la tarjeta de acceso al edificio se almacenara en texto plano, una filtración de la base de datos permitiría abrir todas las puertas. El procesamiento hash impide la duplicación de la tarjeta.
Rutas de inicio y cierre de sesión
Después del registro, las rutas reales de inicio y cierre de sesión:
// Renderizar la página de inicio de sesión
app.get("/login", function(req, res) {
res.send(`
<form action="/login" method="POST">
<input type="email" name="email" placeholder="Correo electrónico" required />
<input type="password" name="password" placeholder="Contraseña" required />
<button type="submit">Iniciar sesión</button>
</form>
`);
});
// Procesar inicio de sesión — Passport ejecuta automáticamente LocalStrategy
app.post("/login",
passport.authenticate("local", {
successRedirect: "/dashboard",
failureRedirect: "/login",
})
);
// Cerrar sesión
app.get("/logout", function(req, res) {
req.logout(function(err) {
if (err) return res.status(500).send("Error al cerrar sesión");
res.redirect("/");
});
});passport.authenticate("local") — Si se especifica el nombre ("local") para LocalStrategy, Passport ejecutará esa estrategia. Si tiene éxito, se redirige a successRedirect; si falla, se redirige a failureRedirect. Aquí, "local" es el nombre predeterminado registrado por el paquete passport-local.
Control de acceso: quién puede ver qué
El propósito principal de un sistema de inicio de sesión es el control de acceso. Permite implementar restricciones como "esta página solo para usuarios autenticados" o "esta función solo para administradores".
// Middleware accesible solo para usuarios autenticados
function requireLogin(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect("/login");
}
// Datos de muestra — Autenticación obligatoria
app.get("/samples/confidential", requireLogin, function(req, res) {
res.json({ data: "Datos confidenciales del experimento", user: req.user.email });
});
// Página pública — Acceso para todos
app.get("/publications", function(req, res) {
res.json({ papers: publicPapers });
});En la investigación biológica, este tipo de escenarios son habituales:
| Escenario | Control de acceso |
|---|---|
| Gestión de muestras internas del laboratorio | Solo los miembros del laboratorio pueden acceder |
| Compartición de datos de investigación colaborativa | Solo los grupos de investigación registrados pueden consultarlos |
| Material de presentación de la investigación | Visible solo para los revisores |
| Sistema de reserva de equipos | Solo los investigadores que hayan iniciado sesión pueden reservar |
| Panel de resultados | El investigador principal tiene acceso completo; los estudiantes, solo a sus propios datos |
Múltiples usuarios y roles
Cuando hay varios usuarios, se necesitan roles:
// Estructura de la tabla de usuarios
// id | email | password_hash | role
// 1 | pi@lab.com | $2b$10$... | admin
// 2 | grad1@lab.com | $2b$10$... | member
// 3 | intern@lab.com | $2b$10$... | viewer
function requireAdmin(req, res, next) {
if (req.isAuthenticated() && req.user.role === "admin") {
return next();
}
res.status(403).json({ error: "Se requieren permisos de administrador" });
}
// Eliminación de muestra — Solo administradores
app.delete("/sample/:id", requireAdmin, function(req, res) {
// Lógica de eliminación
});El investigador principal (profesor) es administrador, los estudiantes de posgrado son miembros y los becarios son usuarios con acceso de solo lectura; esto consiste en trasladar directamente a código la estructura de permisos del laboratorio.
OAuth 2.0: El principio del inicio de sesión social
Seguramente has visto el botón "Iniciar sesión con Google". Esto es OAuth 2.0.
Si explicamos el principio central mediante una analogía, es similar al proceso de emisión de una tarjeta de acceso para un hotel:
1. El huésped (usuario) desea hacer el check-in en el hotel (mi aplicación web)
2. El hotel no tiene capacidad directa para verificar la identidad
3. Hotel: "Por favor, verifique su identidad en el mostrador de recepción (Google) y regrese"
4. El huésped presenta su pasaporte (cuenta/contraseña de Google) en el mostrador de recepción
5. El mostrador emite un código de autorización temporal (Authorization Code)
6. El hotel intercambia el código + su ID de cliente (Client Secret) por una clave de acceso (Access Token)
7. Es posible consultar la información del huésped (nombre, correo electrónico) usando la clave de accesoClave: Mi aplicación web nunca tiene acceso a la contraseña de Google del usuario. Google verifica la identidad y solo transmite un token que confirma: "Esta es la persona correcta".
Los tres actores de OAuth
| Rol | Identidad | Analogía |
|---|---|---|
| Propietario del recurso | Usuario (investigador) | Huésped de hotel |
| Cliente | Mi aplicación web (sistema de gestión de muestras) | Hotel |
| Servidor de recursos | Google, GitHub, etc. | Mostrador de recepción (verificación de identidad) |
Al usar OAuth de Google con Passport.js:
const GoogleStrategy = require("passport-google-oauth20").Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback"
},
function(accessToken, refreshToken, profile, done) {
// profile.emails[0].value → correo electrónico del usuario
// Buscar en la base de datos o crear uno nuevo
done(null, user);
}
));
app.get("/auth/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);clientID y clientSecret se obtienen desde Google Cloud Console. Es el proceso de registro en el que se indica: "Voy a crear esta aplicación y quiero delegar la autenticación de usuarios a Google".
Resumen de los términos clave de OAuth
Client ID y Client Secret
Al registrar una aplicación en Google Cloud Console, se emiten dos elementos:
| Término | Analogía | Función |
|---|---|---|
| Client ID | Número de identificación fiscal | Público. Identifica que "esta aplicación solicita autenticación" |
| Client Secret | Sello comercial | Confidencial. Se utiliza exclusivamente para la autenticación segura entre el servidor y Google |
Si se filtra el Client Secret, otra persona podría hacerse pasar por tu aplicación y enviar solicitudes de autenticación a Google en tu nombre.
Redirect URI (URL de devolución de llamada)
Es la dirección registrada a la que Google redirige después de iniciar sesión, indicando: "autenticación completada, envíame aquí":
https://my-lab-app.com/auth/google/callbackEl URI de redirección registrado en la Consola de Google Cloud y el código callbackURL deben coincidir exactamente. Cualquier diferencia, incluso de un solo carácter, generará un error. Esto es una medida de seguridad para evitar que los resultados de la autenticación se dirijan a sitios incorrectos.
Alcance: la analogía de la llave de valet
El scope de OAuth define "el alcance del acceso permitido".
Piense en un servicio de estacionamiento con valet. Al entregar la llave de valet, se permite arrancar el coche y estacionarlo, pero no se puede abrir el maletero ni la guantera. Se otorgan únicamente los permisos necesarios, de forma limitada.
passport.authenticate("google", { scope: ["profile", "email"] })| alcance | ámbito de acceso | analogía con la llave del coche |
|---|---|---|
profile | nombre, foto de perfil | acceso al asiento del conductor (poder aparcar) |
email | dirección de correo electrónico | consultar el panel de control (ver los contactos) |
drive.readonly | lectura de archivos de Google Drive | abrir el maletero (revisar el equipaje) |
calendar | lectura/escritura del calendario | modificar la agenda dentro del coche |
Para iniciar sesión en nuestro laboratorio, basta con profile y email. Solo necesitamos saber "quién es esta persona". Solicitar ámbitos innecesarios puede generar inquietud en los usuarios y hacer que Google exija una revisión de la aplicación.
Flujo del código de autorización (flujo completo)
Es el método más seguro y común en OAuth 2.0:
1. El usuario hace clic en "Iniciar sesión con Google"
→ El navegador se redirige a la página de inicio de sesión de Google
2. El usuario ingresa su ID/contraseña en Google
→ Google confirma "¿Permitir que esta aplicación proporcione nombre y correo electrónico?"
3. El usuario hace clic en "Permitir"
→ Google transmite el código de autorización a la URI de redireccionamiento de nuestro servidor
4. Nuestro servidor envía el código de autorización + Client Secret a Google
→ Google emite el Token de Acceso
5. Nuestro servidor consulta el perfil del usuario (nombre, correo electrónico) con el Token de Acceso
→ Procesar el inicio de sesión en la función de callback de Passport con esta informaciónClave: El código de autorización es un cupón de un solo uso. Por sí solo, no permite acceder a la información del usuario; debe combinarse con la clave secreta del cliente para obtener un token de acceso. Es seguro siempre que ambos no se vean comprometidos al mismo tiempo.
Configuración de la ruta de Google OAuth
Ruta real de Express:
// 1. El botón "Iniciar sesión con Google" redirige a esta URL
app.get("/auth/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);
// 2. Regresa aquí después de la autenticación de Google (URI de redirección)
app.get("/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/login" }),
function(req, res) {
res.redirect("/dashboard");
}
);Desde la perspectiva del usuario, solo tiene que pulsar el botón "Iniciar sesión con Google". En segundo plano, se ejecutan automáticamente las fases de "Authorization Code" → "Access Token" → consulta del perfil.
¿Por qué usar el inicio de sesión social?
| Ventaja | Explicación |
|---|---|
| Comodidad para el usuario | No es necesario crear una nueva contraseña |
| Delegación de la seguridad | Se delega a Google la carga de almacenar y gestionar contraseñas |
| Fiabilidad | Google ya ofrece autenticación de dos factores y otros mecanismos |
| Implementación rápida | Con la instalación y configuración de la estrategia de Passport, está listo |
Es especialmente útil para las herramientas internas del laboratorio. Dado que los investigadores ya tienen cuentas de Google, pueden acceder directamente con un solo clic en "Iniciar sesión con Google", sin necesidad de registrarse por separado.
Panorama general: capas del sistema de autenticación
Repasemos lo aprendido hasta ahora:
[auth-cookies] Cookies y sesiones — "Recordar este navegador"
↓
[login-system] Inicio de sesión — "Verificar quién eres"
↓
├── Autenticación local (correo electrónico + contraseña + bcrypt)
├── OAuth 2.0 (inicio de sesión social con Google/GitHub)
└── Control de acceso basado en roles (admin/member/viewer)Si las cookies/sesiones son la "memoria", el inicio de sesión es la "verificación" y los roles son los "permisos". Si lo comparamos con un edificio: tener una credencial (cookie) no significa que puedas acceder a todas las áreas. Algunas solo se abren con la tarjeta de identificación del investigador principal, mientras que otras se abren con la tarjeta de identificación de cualquier investigador.
Pruébalo tú mismo (Ejemplo simplificado)
Completa los espacios en blanco del siguiente ejemplo para completar la estrategia de autenticación local de Passport.js.
const passport = require("passport");const = require("passport-local").Strategy;const bcrypt = require("bcrypt");passport.use(new LocalStrategy({ usernameField: "" },function(email, password, done) {db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {if (rows.length === 0) return done(null, );const user = rows[0];const isMatch = bcrypt.(password, user.password_hash);if (!isMatch) return done(null, false);return done(null, );});}));
Siguiente paso
El objetivo de este tema es comprender la estructura y el flujo del sistema de inicio de sesión. En una implementación real:
passport-local+bcrypt— Implementación del inicio de sesión con correo electrónico y contraseñapassport-google-oauth20— Adición del inicio de sesión social de Google- Middleware de roles — Control de acceso como
requireLogin,requireAdmin, etc. - HTTPS — Para evitar que la información de inicio de sesión se transmita en texto plano a través de la red
BioPlayground también utiliza el sistema de autenticación de Supabase. Ya sea que implementes directamente con Passport.js o utilices un BaaS como Supabase, los conceptos aprendidos en este tema (hashing, flujo de OAuth, control basado en roles) se aplican por igual.
Errores comunes y soluciones
P: ¿Qué sucede si guardo las contraseñas sin aplicarles un hash?
En caso de una filtración de la base de datos, todas las contraseñas de los usuarios quedarían expuestas. Dado que muchas personas utilizan la misma contraseña en múltiples sitios, una única filtración puede provocar daños en cadena. El procesamiento mediante hashing no es opcional, sino obligatorio.
P: ¿Dónde se almacena el Client Secret de OAuth?
No se deben incluir directamente en el código. Guárdalos en variables de entorno (archivo .env) y añade .env a .gitignore para evitar que se suban al repositorio Git. Si se filtran, alguien podría hacerse pasar por tu aplicación.
P: ¿Se puede crear un sistema de inicio de sesión sin Passport.js?
Sí, es posible. Puedes comparar las contraseñas con bcrypt y almacenar la información del usuario en la sesión. Sin embargo, tendrás que implementar manualmente el inicio de sesión social, la serialización de sesiones y el manejo de errores, lo que hace que el código sea más complejo. Passport.js es un framework diseñado para reducir esta carga de trabajo repetitivo.
P: ¿Si uso Supabase, no necesito crear esto manualmente?
Correcto. Supabase ofrece autenticación con correo electrónico y contraseña, inicio de sesión social con Google y control de acceso basado en roles mediante una configuración sencilla. Sin embargo, para comprender "qué hace Supabase internamente", es necesario conocer los conceptos de este tema. Incluso al utilizar herramientas, quienes comprenden los principios subyacentes pueden resolver problemas con mayor rapidez.