Recientemente, me dieron apoyo para un proyecto en express.js. Al estudiar el código del proyecto, encontré un trabajo un poco confuso con la autenticación / autorización, que se basaba, como el 99.999% de los casos, en la biblioteca passport.js. Este código funcionó, y siguiendo el principio de "trabajo - no tocar", lo dejé como está. Cuando un par de días después me dieron la tarea de agregar dos estrategias de autorización más. Y luego comencé a recordar que ya estaba haciendo un trabajo similar, y tomó varias líneas de código. Después de revisar la documentación en passport.js, casi no me moví en comprender qué y cómo hacer, porque allí consideraron casos en los que se usa exactamente una estrategia, para lo cual, para cada uno individualmente, se dan ejemplos. Pero cómo combinar varias estrategias, por qué necesita usar el método logIn () (que es lo mismo que login ()), todavía no se ha aclarado. Por lo tanto, para comprender ahora, y no repetir la misma búsqueda una y otra vez, compilé estas notas por mí mismo.
Un poco de historia Inicialmente, las aplicaciones web usaban dos tipos de autenticación / autorización: 1) Básica y 2) usando sesiones usando cookies. En Autenticación / Autorización básica, se envía un encabezado en cada solicitud y, por lo tanto, la autenticación del cliente se realiza en cada solicitud. Cuando se usan sesiones, la autenticación del cliente se realiza solo una vez (los métodos pueden ser muy diferentes, incluidos Basic, así como por nombre y contraseña, que se envían en el formulario, y miles de otros métodos que se llaman estrategias en términos de passport.js). Lo principal es que después de la autenticación, el cliente identifica el identificador de sesión en la cookie (o, en algunas implementaciones, los datos de sesión), y el identificador de usuario se almacena en los datos de sesión.
Primero debe decidir si usará sesiones en su aplicación para autenticación / autorización. Si está desarrollando un back-end para una aplicación móvil, lo más probable es que no. Si se trata de una aplicación web, lo más probable es que sí. Para usar sesiones, debe activar el analizador de cookies, el middleware de sesión y también inicializar la sesión:
const app = express(); const sessionMiddleware = session({ store: new RedisStore({client: redisClient}), secret, resave: true, rolling: true, saveUninitialized: false, cookie: { maxAge: 10 * 60 * 1000, httpOnly: false, }, }); app.use(cookieParser()); app.use(sessionMiddleware); app.use(passport.initialize()); app.use(passport.session());
Aquí debe dar algunas explicaciones importantes. Si no desea que Redis se coma toda la RAM en un par de años, debe encargarse de la eliminación oportuna de los datos de la sesión. El parámetro maxAge es responsable de esto, que establece igualmente este valor tanto para la cookie como para el valor almacenado en redis. Establecer los valores resave: true, rolling: true, extiende el período de validez con el valor maxAge especificado con cada nueva solicitud (si es necesario). De lo contrario, la sesión del cliente se interrumpirá periódicamente. Finalmente, el parámetro saveUninitialized: false no colocará sesiones vacías en redis. Esto le permite colocar la inicialización de sesiones y passport.js en el nivel de aplicación, sin obstruir redis con datos innecesarios. A nivel de ruta, tiene sentido colocar la inicialización solo si el método passport.initialize () necesita ser llamado con diferentes parámetros.
Si no se utilizará la sesión, la inicialización se reducirá significativamente:
app.use(passport.initialize());
A continuación, debe crear un objeto de estrategia (ya que passport.js llama al método de autenticación en terminología). Cada estrategia tiene sus propias características de configuración. Lo único que permanece sin cambios es que la función de devolución de llamada se pasa al constructor de la estrategia, que forma el objeto de usuario, accesible como request.user para las siguientes colas de middleware:
const jwtStrategy = new JwtStrategy(params, (payload, done) => UserModel.findOne({where: {id: payload.userId}}) .then((user = null) => { done(null, user); }) .catch((error) => { done(error, null); }) );
Debemos ser conscientes de que si no se utiliza una sesión, se llamará a este método cada vez que se acceda a un recurso protegido, y una consulta a la base de datos (como en el ejemplo) afectará significativamente el rendimiento de la aplicación.
A continuación, debe dar un comando para usar la estrategia. Cada estrategia tiene un nombre predeterminado. Pero también se puede configurar explícitamente, lo que permite usar una estrategia con diferentes parámetros y la lógica de la función de devolución de llamada:
passport.use('jwt', jwtStrategy); passport.use('simple-jwt', simpleJwtStrategy);
A continuación, para la ruta protegida, debe establecer una estrategia de autenticación y un parámetro de sesión importante (el valor predeterminado es verdadero):
const authenticate = passport.authenticate('jwt', {session: false}); router.use('/hello', authenticate, (req, res) => { res.send('hello'); });
Si no se utiliza la sesión, la autenticación debe estar protegida por todas las rutas de acceso restringido. Si se utiliza la sesión, la autenticación se produce una vez y para ello se establece una ruta especial, por ejemplo, inicio de sesión:
const authenticate = passport.authenticate('local', {session: true}); router.post('/login', authenticate, (req, res) => { res.send({}) ; }); router.post('/logout', mustAuthenticated, (req, res) => { req.logOut(); res.send({}); });
Cuando se usa una sesión, en rutas protegidas, como regla general, se usa un middleware muy conciso (que por alguna razón no está incluido en la biblioteca passport.js):
function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next(); }
Entonces, hubo un último momento: serialización y deserialización del objeto request.user hacia / desde la sesión:
passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; }); });
Quiero enfatizar una vez más que la serialización y la deserialización solo funcionan con estrategias para las cuales se establece el atributo {session: true}. La serialización se realizará exactamente una vez inmediatamente después de la autenticación. Por lo tanto, actualizar los datos almacenados en la sesión será muy problemático, en relación con lo cual solo se guarda la ID de usuario (que no cambia). La deserialización se realizará en cada solicitud a una ruta segura. En este sentido, las consultas a la base de datos (como en el ejemplo) afectan significativamente el rendimiento de la aplicación.
Observación Si usa varias estrategias al mismo tiempo, el mismo código de serialización / deserialización funcionará para todas estas estrategias. Para tener en cuenta la estrategia mediante la cual se realizó la autenticación, por ejemplo, puede incluir un atributo de estrategia en el objeto de usuario. Tampoco tiene sentido llamar al método initialize () varias veces con diferentes valores. Todavía se reescribirá con los valores de la última llamada.
Este podría ser el final de la historia. Porque Además de lo que se ha dicho, en la práctica, no se requiere nada más. Sin embargo, a pedido de los desarrolladores front-end, tuve que agregar un objeto con una descripción del error a la respuesta 401 (por defecto, esta es la línea "No autorizada"). Y esto, como se vio después, no puede hacerse simplemente. Para tales casos, necesita profundizar un poco más en el núcleo de la biblioteca, lo que no es tan agradable. El método passport.authenticate tiene un tercer parámetro opcional: una función de devolución de llamada con la función de firma (error, usuario, información). El pequeño problema es que ni el objeto de respuesta ni ninguna función como done () / next () se pasa a esta función y, por lo tanto, debe convertirlo en middleware usted mismo:
route.post('/hello', authenticate('jwt', {session: false}), (req, res) => { res.send({}) ; }); function authenticate(strategy, options) { return function (req, res, next) { passport.authenticate(strategy, options, (error, user , info) => { if (error) { return next(error); } if (!user) { return next(new TranslatableError('unauthorised', HTTPStatus.UNAUTHORIZED)); } if (options.session) { return req.logIn(user, (err) => { if (err) { return next(err); } return next(); }); } req.user = user; next(); })(req, res, next); }; }
Enlaces utiles:
1)
toon.io/understanding-passportjs-authentication-flow2)
habr.com/post/2012063)
habr.com/company/ruvds/blog/3354344)
habr.com/post/2629795)
habr.com/company/Voximplant/blog/3231606)
habr.com/company/dataart/blog/2628177)
tools.ietf.org/html/draft-ietf-oauth-pop-architecture-088)
oauth.net/articles/authenticationapapacy@gmail.com
4 de enero de 2019