Implementación de tecnología SSO basada en Node.js

Las aplicaciones web se crean utilizando una arquitectura cliente-servidor utilizando HTTP como protocolo de comunicación. HTTP es un protocolo sin estado. Cada vez que un navegador envía una solicitud al servidor, el servidor procesa esa solicitud independientemente de otras solicitudes y no la asocia con solicitudes anteriores o posteriores del mismo navegador. Esto significa, entre otras cosas, que cualquiera puede acceder a los recursos del servidor que no están protegidos de ninguna manera. Si necesita proteger algunos recursos del servidor de personas externas, esto significa que debe limitar de alguna manera lo que el navegador puede solicitar al servidor. Es decir, debe autenticar las solicitudes y responder solo a aquellas que pasaron la verificación, ignorando aquellas que no pasaron la verificación. Para autenticar solicitudes, debe tener cierta información sobre las solicitudes,almacenado en el lado del navegador. Dado que el protocolo HTTP no almacena el estado de las solicitudes, necesitamos algunos mecanismos adicionales para esto que permitan al servidor y al navegador administrar conjuntamente el estado de las conexiones. Estos mecanismos incluyen el uso de cookies, sesiones, JWT.







Si estamos hablando de un solo proyecto web, entonces la información sobre el estado de una sesión particular de interacción entre el cliente y el servidor es fácil de mantener usando la autenticación del usuario en su inicio de sesión. Pero si un sistema tan independiente evoluciona y se convierte en varios sistemas, el desarrollador se enfrenta a la cuestión de mantener información sobre el estado de cada uno de estos sistemas separados. En la práctica, esta pregunta se ve así: "¿El usuario de estos sistemas tendrá que ingresar a cada uno de ellos por separado y también salir de ellos?"



Existe una buena regla general sobre los sistemas que aumentan en complejidad con el tiempo y cómo esos sistemas interactúan con sus usuarios. Es decir, la carga de resolver los problemas asociados con la complicación de la arquitectura del proyecto recae en el sistema y no en sus usuarios. No importa cuán complejos sean los mecanismos internos del proyecto web. El usuario debería verlo como un sistema unificado. En otras palabras, un usuario que trabaja con un sistema web que consta de muchos componentes debería percibir lo que está sucediendo como si estuviera trabajando con un sistema. En particular, estamos hablando de autenticación en dichos sistemas que utilizan SSO (inicio de sesión único), una tecnología de inicio de sesión único.



¿Cómo creo sistemas que usan SSO? Puede pensar en la vieja solución basada en cookies aquí, pero esta solución está sujeta a limitaciones. Las restricciones se aplican a los dominios desde los que se establecen las cookies. Solo se puede eludir si se recopilan todos los nombres de dominio de todos los subsistemas de la aplicación web en un dominio de nivel superior.



En el entorno actual, estas soluciones se ven obstaculizadas por la adopción generalizada de arquitecturas de microservicios. La gestión de sesiones se volvió más complicada en un momento en el que se usaban diferentes tecnologías en el desarrollo de proyectos web y cuando en ocasiones se alojaban diferentes servicios en diferentes dominios. Además, los servicios web que solían estar escritos en Java comenzaron a escribir utilizando las capacidades de la plataforma Node.js. Esto hizo más difícil trabajar con cookies. Resultó que las sesiones ahora no son tan fáciles de administrar.



Estas dificultades han llevado al desarrollo de nuevos métodos de inicio de sesión en sistemas, en particular, estamos hablando de la tecnología de inicio de sesión único.



Tecnología de inicio de sesión único



El principio básico en el que se basa la tecnología de inicio de sesión único es que un usuario puede iniciar sesión en un sistema de un proyecto que consta de muchos sistemas y ser autorizado en todos los demás sistemas sin tener que iniciar sesión de nuevo. Al mismo tiempo, estamos hablando de una salida centralizada de todos los sistemas.



Nosotros, con fines educativos, implementaremos la tecnología SSO en la plataforma Node.js.



Cabe señalar que la implementación de esta tecnología a escala corporativa requerirá mucho más esfuerzo del que vamos a poner en el desarrollo de nuestro sistema de capacitación. Es por eso que existen soluciones SSO especializadas diseñadas para proyectos a gran escala.



¿Cómo se organiza el inicio de sesión SSO?



En el corazón de la implementación de SSO se encuentra un servidor de autenticación único e independiente que puede aceptar información para autenticar a los usuarios. Por ejemplo: dirección de correo electrónico, nombre de usuario, contraseña. Otros sistemas no proporcionan al usuario mecanismos directos para iniciar sesión en ellos. Autorizan al usuario indirectamente al recibir información sobre él desde el servidor de autenticación. Los mecanismos de autorización indirecta se implementan mediante tokens.



Aquí está el repositorio de código para el proyecto simple-sso, cuya implementación describiré aquí. Estoy usando el framework Node.js, pero puedes implementar el mismo usando algo diferente. Hagamos un análisis paso a paso de las acciones del usuario que trabaja con el sistema y los mecanismos que componen este sistema.



Paso 1



El usuario intenta acceder a un recurso protegido en el sistema (llamemos a este recurso el "consumidor SSO", "sso-consumidor"). El consumidor de SSO descubre que el usuario no ha iniciado sesión y lo redirige al "servidor SSO" ("sso-server") utilizando su propia dirección como parámetro de consulta. Un usuario autenticado correctamente será redirigido a esta dirección. Este mecanismo lo proporciona el middleware Express:



const isAuthenticated = (req, res, next) => {
  //   ,   ,
  //     -     SSO-     
  //    URL  URL,     
  // ,   
  const redirectURL = `${req.protocol}://${req.headers.host}${req.path}`;
  if (req.session.user == null) {
    return res.redirect(
      `http://sso.ankuranand.com:3010/simplesso/login?serviceURL=${redirectURL}`
    );
  }
  next();
};

module.exports = isAuthenticated;


Paso 2



El servidor SSO descubre que el usuario no ha iniciado sesión y lo redirige a la página de inicio de sesión:



const login = (req, res, next) => {
  //  req.query  url,      
  //    ,     sso-.
  //        
  //     
  const { serviceURL } = req.query;
  //         URL.
  if (serviceURL != null) {
    const url = new URL(serviceURL);
    if (alloweOrigin[url.origin] !== true) {
      return res
        .status(400)
        .json({ message: "Your are not allowed to access the sso-server" });
    }
  }
  if (req.session.user != null && serviceURL == null) {
    return res.redirect("/");
  }
  //          -  
  //   
  if (req.session.user != null && serviceURL != null) {
    const url = new URL(serviceURL);
    const intrmid = encodedId();
    storeApplicationInCache(url.origin, req.session.user, intrmid);
    return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
  }

  return res.render("login", {
    title: "SSO-Server | Login"
  });
};


Haré algunos comentarios aquí sobre seguridad.



Verificamos el serviceURLparámetro de solicitud entrante al servidor SSO. Esto nos permite saber si esta URL está registrada en el sistema y si el servicio que representa puede utilizar los servicios de un servidor SSO.



Así es como se vería una lista de URL para servicios que pueden usar el servidor SSO:



const alloweOrigin = {
"http://consumer.ankuranand.in:3020": true,
"http://consumertwo.ankuranand.in:3030": true,
"http://test.tangledvibes.com:3080": true,
"http://blog.tangledvibes.com:3080": fasle,
};


Paso 3



El usuario ingresa un nombre de usuario y una contraseña que se envían al servidor SSO en la solicitud de inicio de sesión.





Página de inicio de sesión



Paso 4



El servidor de autenticación SSO verifica la información del usuario y crea una sesión entre él y el usuario. Esta es la llamada "sesión global". Se crea un token de autorización de inmediato. El token es una cadena de caracteres aleatorios. No importa exactamente cómo se genera esta cadena. Lo principal es que las líneas similares no se repiten para diferentes usuarios y que esa línea sería difícil de forjar.



Paso 5



El servidor SSO toma el token de autorización y lo pasa al lugar de donde vino el usuario que acaba de iniciar sesión (es decir, pasa el token al consumidor SSO).



const doLogin = (req, res, next) => {
  //         .
  //         , 
  // userDB -   ,   ,   
  const { email, password } = req.body;
  if (!(userDB[email] && password === userDB[email].password)) {
    return res.status(404).json({ message: "Invalid email and password" });
  }

  //     
  const { serviceURL } = req.query;
  const id = encodedId();
  req.session.user = id;
  sessionUser[id] = email;
  if (serviceURL == null) {
    return res.redirect("/");
  }
  const url = new URL(serviceURL);
  const intrmid = encodedId();
  storeApplicationInCache(url.origin, id, intrmid);
  return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
};


De nuevo, algunas notas de seguridad:



  • Este token siempre debe considerarse como un mecanismo intermedio, se utiliza para obtener otro token.
  • Si está utilizando un JWT como token intermedio, intente no incluir secretos en él.


Paso 6



El consumidor de SSO recibe un token y se comunica con el servidor de SSO para verificar el token. El servidor verifica el token y devuelve otro token con información del usuario. El consumidor de SSO utiliza este token para crear una sesión con el usuario. Esta sesión se llama local.



Este es el código de middleware que se usa en el consumidor SSO basado en Express:



const ssoRedirect = () => {
  return async function(req, res, next) {
    // ,    req queryParameter,  ssoToken,
    //  ,    .
    const { ssoToken } = req.query;
    if (ssoToken != null) {
      //   ssoToken   ,  .
      const redirectURL = url.parse(req.url).pathname;
      try {
        const response = await axios.get(
          `${ssoServerJWTURL}?ssoToken=${ssoToken}`,
          {
            headers: {
              Authorization: "Bearer l1Q7zkOL59cRqWBkQ12ZiGVW2DBL"
            }
          }
        );
        const { token } = response.data;
        const decoded = await verifyJwtToken(token);
        //      jwt,  
        // global-session-id  id ,  
        //         .
        req.session.user = decoded;
      } catch (err) {
        return next(err);
      }

      return res.redirect(`${redirectURL}`);
    }

    return next();
  };
};


Después de recibir una solicitud de un consumidor de SSO, el servidor verifica la existencia del token y su fecha de vencimiento. El token validado se considera válido.



En nuestro caso, el servidor SSO, después de la verificación exitosa del token, devuelve un JWT firmado con información sobre el usuario.



const verifySsoToken = async (req, res, next) => {
  const appToken = appTokenFromRequest(req);
  const { ssoToken } = req.query;
  //        ssoToken .
  //  ssoToken    - ,   .
  if (
    appToken == null ||
    ssoToken == null ||
    intrmTokenCache[ssoToken] == null
  ) {
    return res.status(400).json({ message: "badRequest" });
  }

  //  appToken  -     
  const appName = intrmTokenCache[ssoToken][1];
  const globalSessionToken = intrmTokenCache[ssoToken][0];
  //  appToken   ,   SSO-        
  if (
    appToken !== appTokenDB[appName] ||
    sessionApp[globalSessionToken][appName] !== true
  ) {
    return res.status(403).json({ message: "Unauthorized" });
  }
  // ,     
  const payload = generatePayload(ssoToken);

  const token = await genJwtToken(payload);
  //    ,     
  delete intrmTokenCache[ssoToken];
  return res.status(200).json({ token });
};


Aquí hay algunas notas de seguridad.



  • Todas las aplicaciones que utilizarán este servidor para la autenticación deben estar registradas en el servidor SSO. Es necesario que se les asignen códigos que se utilizarán para verificarlos cuando realicen solicitudes al servidor. Esto permite un mayor nivel de seguridad al comunicarse entre el servidor SSO y los consumidores SSO.
  • Es posible generar diferentes archivos rsa "privados" y "públicos" para cada aplicación y dejar que cada uno de ellos verifique sus JWT internamente con sus respectivas claves públicas.


Además, puede definir una política de seguridad a nivel de aplicación y organizar su almacenamiento centralizado:



const userDB = {
  "info@ankuranand.com": {
    password: "test",
    userId: encodedId(), //   ,         .
    appPolicy: {
      sso_consumer: { role: "admin", shareEmail: true },
      simple_sso_consumer: { role: "user", shareEmail: false }
    }
  }
};


Después de que el usuario inicia sesión con éxito en el sistema, se crean sesiones entre él y el servidor SSO, así como entre él y cada subsistema. La sesión establecida entre el usuario y el servidor SSO se denomina sesión global. Una sesión establecida entre un usuario y un subsistema que proporciona al usuario algunos servicios se denomina sesión local. Una vez establecida la sesión local, el usuario podrá trabajar con los recursos del subsistema cerrados a recursos extraños.





Configurar sesiones locales y globales



Un recorrido rápido por el consumidor SSO y el servidor SSO



Hagamos un recorrido rápido por el consumidor SSO y la funcionalidad del servidor SSO.



▍ Consumidor SSO



  1. El subsistema de consumidor SSO no autentica al usuario redirigiéndolo al servidor SSO.
  2. Este subsistema recibe el token que le pasa el servidor SSO.
  3. Interactúa con el servidor para verificar la validez del token.
  4. Ella recibe el JWT y valida este token usando la clave pública.
  5. Este subsistema establece una sesión local.


Servidor ▍SSO



  1. El servidor SSO valida la información de inicio de sesión del usuario.
  2. El servidor crea una sesión global.
  3. Crea un token de autorización.
  4. Se envía un token de autorización al consumidor de SSO.
  5. El servidor verifica la validez de los tokens que le pasan los consumidores de SSO.
  6. El servidor envía un SSO JWT al consumidor con información del usuario.


Organización del cierre de sesión centralizado



De manera similar a cómo se implementó SSO, puede implementar la tecnología SSO. Aquí solo debes tener en cuenta las siguientes consideraciones:



  1. Si existe una sesión local, también debe existir una sesión global.
  2. Si existe una sesión global, no significa necesariamente que exista una sesión local.
  3. Si se destruye la sesión local, también se debe destruir la sesión global.


Salir



Como resultado, se puede observar que hay muchas implementaciones listas para usar de tecnología de inicio de sesión único que puede integrar en su sistema. Todos tienen sus propias ventajas y desventajas. Desarrollar un sistema de este tipo de forma independiente, desde cero, es un proceso iterativo durante el cual es necesario analizar las características de cada uno de los sistemas. Esto incluye métodos de inicio de sesión, almacenamiento de información del usuario, sincronización de datos y más.



¿Sus proyectos utilizan mecanismos SSO?






All Articles