Autenticación de usuarios con Node.js

Esta parte del instructivo de Bookshelf en Node.js muestra cómo crear un flujo de acceso para los usuarios y cómo usar la información del perfil para ofrecer a los usuarios una funcionalidad personalizada.

Mediante la Plataforma de Google Identity, podrás acceder fácilmente a la información de tus usuarios y, a la vez, garantizar que Google administra sus credenciales de acceso de manera segura. Con OAuth 2.0, brindar un flujo de acceso a todos los usuarios de tu app es fácil y, además, permite a tu aplicación acceder a la información del perfil básico de los usuarios autenticados.

Esta página forma parte de un instructivo de varias páginas. Ve a la aplicación Bookshelf en Node.js para comenzar desde el principio y leer las instrucciones de configuración.

Crear un ID de cliente de aplicación web

Mediante un ID de cliente de aplicación web, tu aplicación puede autorizar usuarios y acceder a las API de Google en nombre de ellos.

  1. En Google Cloud Platform Console, ve a Credenciales.

    Credenciales.

  2. Haz clic en la pantalla de consentimiento de OAuth. Ingresa Node.js Bookshelf App como el nombre del producto.

  3. En Dominios autorizados, agrega el nombre de tu aplicación de App Engine como [YOUR_PROJECT_ID].appspot.com. Reemplaza [YOUR_PROJECT_ID] por el ID del proyecto de GCP.

  4. Llena todos los campos opcionales de relevancia. Haz clic en Guardar.

  5. Haz clic en Crear credenciales > ID de cliente de OAuth.

  6. En la lista desplegable Tipo de aplicación, haz clic en Aplicación web.

  7. En el campo Nombre, ingresa Node.js Bookshelf Client.

  8. En el campo URI de redireccionamiento autorizado, ingresa las siguientes URL, una a la vez.

    http://localhost:8080/auth/google/callback
    http://[YOUR_PROJECT_ID].appspot.com/auth/google/callback
    https://[YOUR_PROJECT_ID].appspot.com/auth/google/callback

  9. Haz clic en Crear.

  10. Copia el ID de cliente y el secreto de cliente y guárdalos para después.

Crear una instancia de Memcached

A fin de almacenar las sesiones a través de varias instancias de App Engine de manera adecuada, los datos de las sesiones deben almacenarse en ubicaciones a las que todas las instancias de App Engine puedan acceder. Para ello, usaremos una instancia de Memcached a fin de almacenar nuestros datos de sesiones entre servidores.

Si aún no tienes una instancia de Memcached consigue una gratis en Redis Labs. Si estás usando Redis Labs, la propiedad Endpoint indica la URL que dirige a tu instancia de Memcached.

Configuraciones

Copia el archivo config.json desde la sección Cloud Storage de este instructivo al directorio nodejs-getting-started/4-auth. Agrega las líneas siguientes al archivo copiado:

"OAUTH2_CLIENT_ID": "[YOUR_OAUTH2_CLIENT_ID]",
"OAUTH2_CLIENT_SECRET": "[YOUR_OAUTH2_CLIENT_SECRET]",
"OAUTH2_CALLBACK": "http://localhost:8080/auth/google/callback",
"MEMCACHE_URL": "[YOUR_MEMCACHE_URL]"
  1. Reemplaza [YOUR_OAUTH2_CLIENT_ID] y [YOUR_OAUTH2_CLIENT_SECRET] por el ID de cliente de la aplicación y el secreto de cliente que creaste anteriormente.
  2. Reemplaza [YOUR_MEMCACHE_URL] por una URL que dirija a tu instancia de Memcached. Esta URL debe estar en la forma hostname:port.
  3. Si tu instancia de Memcached requiere usuario y contraseña, agrega también "MEMCACHE_USERNAME": "[YOUR_MEMCACHE_USERNAME]" y "MEMCACHE_PASSWORD": "[YOUR_MEMCACHE_PASSWORD]" a config.json. Reemplaza [YOUR_MEMCACHE_USERNAME] y [YOUR_MEMCACHE_PASSWORD] con tus valores.

Instalar dependencias

Usa npm para instalar las dependencias en el directorio nodejs-getting-started/4-auth:

    npm install

Ejecutar la aplicación en la máquina local

  1. Usa npm para iniciar un servidor web local:

        npm start
    
  1. En el navegador web, ingresa la siguiente dirección:

    http://localhost:8080

Ahora podrás navegar por las páginas web de la aplicación, acceder con la cuenta de Google, agregar libros y ver los libros que agregaste mediante el vínculo Mis libros en la barra de navegación superior.

Implementar la aplicación en el entorno estándar de App Engine

  1. Para habilitar la autenticación en línea, cambia la línea OAUTH2_CALLBACK del archivo config.json en el directorio nodejs-getting-started/4-auth a lo siguiente. Reemplaza [YOUR_PROJECT_ID] por el ID del proyecto:

    "OAUTH2_CALLBACK": "https://[YOUR_PROJECT_ID].appspot.com/auth/google/callback"
    
  2. Implementa la app de muestra desde el directorio nodejs-getting-started/4-auth:

    gcloud app deploy
    
  3. En el navegador web, ingresa la siguiente dirección. Reemplaza [YOUR_PROJECT_ID] por el ID del proyecto:

    https://[YOUR_PROJECT_ID].appspot.com
    

Si actualizas tu app, podrás implementar la versión actualizada mediante el mismo comando que usaste para implementar la app por primera vez. La implementación nueva crea una versión nueva de la app y la convierte a la versión predeterminada. Se conservan las versiones anteriores de la aplicación. De forma predeterminada, el entorno estándar de App Engine escala a 0 instancias cuando no hay tráfico entrante a una versión, por lo que las versiones sin usar no deben tener costo. Sin embargo, todas estas versiones de la aplicación son recursos facturables.

Consulta la sección Limpiar en el paso final de este instructivo para obtener más información sobre la limpieza de recursos facturables, incluidas las versiones de la aplicación no predeterminadas.

Estructura de la aplicación

El siguiente diagrama muestra los componentes de la aplicación y la manera en que se conectan entre sí.

Estructura de muestra de Auth

Comprensión del código

En esta sección se explica el código de la aplicación y su funcionamiento.

Acerca de las sesiones

Antes de que tu aplicación pueda autenticar usuarios, necesitas un modo de almacenar la información del usuario actual en una sesión. Express incluye una implementación basada en la memoria, pero no es apta para una aplicación que se puede entregar desde varias instancias, ya que una sesión registrada en una de ellas podría ser diferente a la de otras instancias. La muestra de Bookshelf usa un objeto MemcachedStore para almacenar los datos de las sesiones en un servicio Memcached remoto especificado en config.json.

// Configure the session and session storage.
const sessionConfig = {
  resave: false,
  saveUninitialized: false,
  secret: config.get('SECRET'),
  signed: true
};

// In production use the Memcache instance to store session data,
// otherwise fallback to the default MemoryStore in development.
if (config.get('NODE_ENV') === 'production' && config.get('MEMCACHE_URL')) {
  if (config.get('MEMCACHE_USERNAME') && (config.get('MEMCACHE_PASSWORD'))) {
    sessionConfig.store = new MemcachedStore({
      servers: [config.get('MEMCACHE_URL')],
      username: config.get('MEMCACHE_USERNAME'),
      password: config.get('MEMCACHE_PASSWORD')});
  } else {
    sessionConfig.store = new MemcachedStore({
      servers: [config.get('MEMCACHE_URL')]
    });
  }
}

app.use(session(sessionConfig));

Autenticación de usuarios

La muestra de Bookshelf usa Passport.js para administrar la autenticación de los usuarios. Es necesario configurar Passport.js antes de que pueda autenticar usuarios:

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

function extractProfile (profile) {
  let imageUrl = '';
  if (profile.photos && profile.photos.length) {
    imageUrl = profile.photos[0].value;
  }
  return {
    id: profile.id,
    displayName: profile.displayName,
    image: imageUrl
  };
}

// Configure the Google strategy for use by Passport.js.
//
// OAuth 2-based strategies require a `verify` function which receives the
// credential (`accessToken`) for accessing the Google API on the user's behalf,
// along with the user's profile. The function must invoke `cb` with a user
// object, which will be set at `req.user` in route handlers after
// authentication.
passport.use(new GoogleStrategy({
  clientID: config.get('OAUTH2_CLIENT_ID'),
  clientSecret: config.get('OAUTH2_CLIENT_SECRET'),
  callbackURL: config.get('OAUTH2_CALLBACK'),
  accessType: 'offline'
}, (accessToken, refreshToken, profile, cb) => {
  // Extract the minimal profile information we need from the profile object
  // provided by Google
  cb(null, extractProfile(profile));
}));

passport.serializeUser((user, cb) => {
  cb(null, user);
});
passport.deserializeUser((obj, cb) => {
  cb(null, obj);
});

Los alcances indican los elementos de la información del usuario a los que tu aplicación tiene permitido acceder. Google ofrece varios servicios y un conjunto de alcances para cada servicio. Por ejemplo, hay un alcance que permite acceso de solo lectura a Google Drive y otro alcance que permite acceso de lectura y escritura. Esta muestra solo requiere los alcances básicos email y profile, que otorgan a la aplicación el acceso a la dirección de correo electrónico y a la información del perfil básico del usuario.

La autenticación del usuario implica dos pasos básicos que en conjunto se denominan flujo de servicio web:

  1. Redireccionamiento del usuario al servicio de autorización de Google.
  2. Procesamiento de la respuesta cuando Google redirecciona al usuario de vuelta a tu aplicación.

Redireccionamiento a Google

Tu aplicación envía al usuario al servicio de autorización de Google. La aplicación usa tu ID de cliente y los alcances que necesita para generar una URL.

router.get(
  // Login url
  '/auth/login',

  // Save the url of the user's current page so the app can redirect back to
  // it after authorization
  (req, res, next) => {
    if (req.query.return) {
      req.session.oauth2return = req.query.return;
    }
    next();
  },

  // Start OAuth 2 flow using Passport.js
  passport.authenticate('google', { scope: ['email', 'profile'] })
);

Luego, tu aplicación redirecciona al usuario a la URL generada. Se le solicita que permita que tu aplicación acceda a los alcances especificados. Tras aceptar, se redirecciona al usuario de vuelta a tu aplicación.

Procesamiento de la respuesta de autorización

Google envía al usuario de vuelta a tu aplicación junto con un código de autorización. Este código y tu secreto de cliente se pueden intercambiar por las credenciales del usuario y por otra información acerca de él. Este paso del flujo ocurre en su totalidad en el servidor, sin necesidad de redireccionar al usuario. Este paso adicional es obligatorio a fin de verificar que el código de autorización es auténtico y que es tu aplicación la que solicita credenciales. Passport.js facilita este paso:

router.get(
  // OAuth 2 callback url. Use this url to configure your OAuth client in the
  // Google Developers console
  '/auth/google/callback',

  // Finish OAuth 2 flow using Passport.js
  passport.authenticate('google'),

  // Redirect back to the original page, if any
  (req, res) => {
    const redirect = req.session.oauth2return || '/';
    delete req.session.oauth2return;
    res.redirect(redirect);
  }
);

Una vez que obtengas las credenciales, podrás almacenar información acerca del usuario. Passport.js serializa automáticamente al usuario a la sesión. Una vez que la información del usuario esté en la sesión, podrás efectuar algunas funciones de middleware para que trabajar con autenticación sea más sencillo.

// Middleware that requires the user to be logged in. If the user is not logged
// in, it will redirect the user to authorize the application and then return
// them to the original URL they requested.
function authRequired (req, res, next) {
  if (!req.user) {
    req.session.oauth2return = req.originalUrl;
    return res.redirect('/auth/login');
  }
  next();
}

// Middleware that exposes the user's profile as well as login/logout URLs to
// any templates. These are available as `profile`, `login`, and `logout`.
function addTemplateVariables (req, res, next) {
  res.locals.profile = req.user;
  res.locals.login = `/auth/login?return=${encodeURIComponent(req.originalUrl)}`;
  res.locals.logout = `/auth/logout?return=${encodeURIComponent(req.originalUrl)}`;
  next();
}

Usa la información que proporcionó el middleware en tus plantillas para indicarle al usuario que accedió o salió.

if profile
  if profile.image
    img.img-circle(src=profile.image, width=24)
  span #{profile.displayName}  
    a(href=logout) (logout)
else
  a(href=login) Login

Personalización

Ahora que la información del usuario que accedió está disponible mediante el middleware, podrás hacer un seguimiento de los libros que agrega cada usuario a la base de datos.

router.post(
  '/add',
  images.multer.single('image'),
  images.sendUploadToGCS,
  (req, res, next) => {
    const data = req.body;

    // If the user is logged in, set them as the creator of the book.
    if (req.user) {
      data.createdBy = req.user.displayName;
      data.createdById = req.user.id;
    } else {
      data.createdBy = 'Anonymous';
    }

    // Was an image uploaded? If so, we'll use its public URL
    // in cloud storage.
    if (req.file && req.file.cloudStoragePublicUrl) {
      data.imageUrl = req.file.cloudStoragePublicUrl;
    }

    // Save the data to the database.
    getModel().create(data, (err, savedData) => {
      if (err) {
        next(err);
        return;
      }
      res.redirect(`${req.baseUrl}/${savedData.id}`);
    });
  }
);

Ahora que tienes esa información en la base de datos, puedes mostrarle al usuario los libros que agregó personalmente a la base de datos.

// Use the oauth2.required middleware to ensure that only logged-in users
// can access this handler.
router.get('/mine', oauth2.required, (req, res, next) => {
  getModel().listBy(
    req.user.id,
    10,
    req.query.pageToken,
    (err, entities, cursor, apiResponse) => {
      if (err) {
        next(err);
        return;
      }
      res.render('books/list.pug', {
        books: entities,
        nextPageToken: cursor
      });
    }
  );
});

Este código usa un método de modelo nuevo llamado listBy. La implementación depende del backend de base de datos que elijas.

Datastore

function listBy (userId, limit, token, cb) {
  const q = ds.createQuery([kind])
    .filter('createdById', '=', userId)
    .limit(limit)
    .start(token);

  ds.runQuery(q, (err, entities, nextQuery) => {
    if (err) {
      cb(err);
      return;
    }
    const hasMore = nextQuery.moreResults !== Datastore.NO_MORE_RESULTS ? nextQuery.endCursor : false;
    cb(null, entities.map(fromDatastore), hasMore);
  });
}

Cloud SQL

function listBy (userId, limit, token, cb) {
  token = token ? parseInt(token, 10) : 0;
  connection.query(
    'SELECT * FROM `books` WHERE `createdById` = ? LIMIT ? OFFSET ?',
    [userId, limit, token],
    (err, results) => {
      if (err) {
        cb(err);
        return;
      }
      const hasMore = results.length === limit ? token + results.length : false;
      cb(null, results, hasMore);
    });
}
¿Te sirvió esta página? Envíanos tu opinión:

Enviar comentarios sobre…