POSTS

Agrega autenticación con Facebook a tu API en Nodejs

Introducción

Como un usuario que usa frecuentemente Facebook para registrarse o iniciar sesion en muchas aplicaciones, hoy en dia veo muy necesario que una nueva aplicación incluya al menos para sus usuarios una alternativa diferente al login tradicional de usuario/password con cualquier red social (Sea esta Facebook, Twitter o Google).

Ya que estas opciones mejoran notablemente la experiencia de los usuarios con nuestras aplicaciones al permitir a los usuarios registrarse y comenzar a usar nuestros servicios con un solo click, en este tutorial aprenderemos como agregar soporte para iniciar sesión con Facebook en nuestra API usando Express y Passport, por lo tanto debes tener instalado en tu computador nodejs y npm.

Para mantener este tutorial lo mas simple posible no verás ejemplos de cómo implementarlo en Angular, React, iOS o cualquier otra tecnología ya que estos deberían pertenecer a otro tutorial. Además no usaremos ninguna base de datos en específico aunque el proceso para integrar una es relativamente sencillo, esto para no mezclar conceptos y lenguajes, tambien se asume que el lector tiene algo de experiencia con express.

Librerías

Para implementar soporte para login con Facebook usaremos las siguientes librerías, las cuales son muy populares actualmente:

Passport

Es una librería extremadamente modular que nos permitirá agregar autenticacion y autorizacion a nuestra API sin necesidad de ensuciar demasiado nuestra lógica de negocio ya que esta funciona como un simple middleware de express.

Además es sencillo de soportar otros proveedores en el futuro como Google o Twitter como login (Recuerda que esta API será completamente agnóstica del tipo de cliente) agregando nuevas estrategias que no son más que diferentes formas de loguear a nuestros usuarios en nuestro servicio.

Jsonwebtoken

Ya que nuestra API no preservara estado entre diferentes peticiones, necesitaremos una forma de reconocer que usuario esta generando la petición, esto lo podemos lograr usando JWT o Json Web Tokens.

Passport-facebook-token

Usaremos la estrategia de passport-facebook-token en vez de la tradicional passport-facebook, esto es porque la librería passport-facebook está desarrollada originalmente para proyectos web tradicionales donde toda la lógica se llevará a cabo en el servidor, el cual no es nuestro caso ya que nuestra lógica se distribuirá entre los clientes y el servidor.

[Passport-jwt]()

Usaremos esta estrategia para crear rutas que necesiten un usuario logueado.

Crea una aplicacion en Facebook

Ten en cuenta que para seguir este tutorial necesitaras crear una aplicacion en Facebook Facebook for developers y contar con el AppID y el AppSecret de tu aplicacion.

Flujo de autenticacion

En esta sección describiré el proceso cómo funciona nuestra API y cómo se comunicara con el cliente:

  • El usuario hace click en el botón de “Continuar con FB”, dependiendo de la plataforma del cliente este usara el SDK de Facebook para obtener el accessToken del usuario que realizo el click.
  • El cliente enviara una petición a la url /auth/facebook/token con el accessToken del usuario que está intentando realizar la petición.
  • El servidor validará que el accessToken enviado sea válido y obtendra o creará (si es necesario) el registro en la base de datos (En este caso un push a un array).
  • El servidor generará un nuevo JWT con los datos necesarios del usuario y lo enviará al cliente.
  • El cliente enviará en todos las nuevas peticiones el token obtenido anteriormente con el formato Bearer Token en el header Authorization de la petición.
  • El servidor usará este token para autorizar o denegar el acceso a un recurso de nuestra API.

Implementación

Creando nuestro proyecto de prueba

En esta sección crearemos nuestra carpeta donde tendremos nuestro proyecto, estos comandos crearan una carpeta donde estara nuestro código y también crearemos el archivo package.json donde guardaremos todas las dependencias de la API.

mkdir test-facebook && cd test-facebook && touch app.js auth.js && npm init --y

Instalando dependencias

Luego instalaremos las dependencias que necesitamos, las cuales son:

npm install --save express body-parser compression passport passport-facebook-token passport-jwt jsonwebtoken

Luego de correr este comando deberias ver como el archivo package.json tiene listadas estas dependencias.

Configurando nuestra estrategia de Facebook token

En nuestro editor favorito (El mio es VSCode con la extension VIM) comenzaremos configurando la estrategia de passport-facebook-token.

El archivo base con el que trabajaremos (El cual creaste con los comandos anteriores) debemos agregar el siguiente codigo para que luzca asi:

//app.js

// Codigo normal en cualquier API en express
const express = require('express');
const compression = require('compression');
const bodyParser = require('body-parser');

// En este archivo tendremos toda la configuracion referente a autenticacion
const passport = require('./auth');

// Normal code
const app = express();

app.set("port", 3000)
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req, res) => {
    return res.send('Todo funcionando correctamente');
});

app.listen(3000, () => {
    console.log('La app esta corriendo en dev');
});

En este paso solo importamos las librerias necesarias para trabajar, y creamos nuestro servidor de express asi que desde este momento deberias poder correr el servidor con

node app.js

O tambien puedes usar nodemon (Antes debes tenerla instalada globalmente) si no quieres reiniciar manualmente el servidor cada vez que hagamos cambios

nodemon app.js

Luego de correr el comando deberias poder acceder a la ruta http://localhost:3000 y ver el mensaje

Todo funcionando correctamente

Ahora comenzaremos a configurar la estrategia de passport-facebook-token, esto lo haremos en el archivo auth.js (Que tambien deberia estar creado si corriste los comandos anteriores).

En este archivo importaremos todas las dependencias que necesitaremos para la autenticacion y configuraremos la estrategia con el siguiente codigo:

//auth.js

// Importamos las librerias que usaremos
const passport = require('passport');
const jwt = require('jsonwebtoken');
const FacebookTokenStrategy = require('passport-facebook-token');
const PassportJWT = require('passport-jwt');
const JWTStrategy = PassportJWT.Strategy;
const ExtractJwt = PassportJWT.ExtractJwt;

passport.use(new FacebookTokenStrategy({
  clientID: 'INSERTA_TU_CLIENT_ID_AQUI',
  clientSecret: 'INSERTA_TU_CLIENT_SECRET_AQUI',
}, 
(accessToken, refreshToken, profile, cb) => {
 //...
}));

Notaras el tipico patron de nodejs de un callback al final de la expresion, la forma en que una funcion recibe sus argumentos es llamada signature y para este callback en especifico su signature es el siguiente:

Callback signature

(acessToken:string, refreshToken:string, profile:Object, cb:Function) => 
````
<br/>

Normalmente dentro de esta funcion querras validar si el usuario ya existe en tu base de datos y si no existe crearlo basandote en el facebook id.

**acessToken** 
El accessToken es simplemente el accessToken que el cliente obtuvo usando los SDK de Facebook para su plataforma y que luego nos lo envio.

**refreshToken**
Token opcional que nos envia el cliente y que nos permitira refrescar el token una vez el accessToken expire.

**profile**
Objeto con la informacion del usuario (En este caso la mia), en el siguiente formato, [Mas informacion](http://www.passportjs.org/docs/profile/)

```javascript
 { provider: 'facebook',
  id: 'facebook id',
  displayName: 'Miguel Crespo',
  name: { familyName: 'Crespo', givenName: 'Miguel', middleName: '' },
  gender: '',
  emails: [ { value: 'tu email' } ],
  photos:
   [ { value: 'https://graph.facebook.com/v2.6/10205547895644317/picture?type=large' } ],
  _raw: '{"id":"facebook id","name":"Miguel Crespo","last_name":"Crespo","first_name":"Miguel","email":"tu email"}',
  _json:
   { id: 'facebook id',
     name: 'Miguel Crespo',
     last_name: 'Crespo',
     first_name: 'Miguel',
     email: 'tu email' } }

cb Funcion que deberemos llamar una vez nuestra logica termine dentro del callback y que usaremos para comunicarle a passport si la validacion tuvo o no exito.

Las formas de llamar esta funcion son las siguiente:

cb(error) Cuando llamamos el callback con un solo argumento que no sea null estamos comunicando que hubo un error en el codigo, nota que este error es muy diferente a un error de autenticacion, este error es mas un error de codigo que de credenciales, por lo cual se respondera inmediatamente con un HTTP 5xx.

cb(null, false) De esta manera estamos comunicando que la validacion fallo por x o y motivo.

cb(null, user) Siendo user el objeto del usuario, de esta manera estamos comunicando que la autenticacion fue exitosa y passport se encargara de setear el valor del usuario al req.user de la peticion, haciendolo disponible para las proximas rutas que manejen esta peticion.

En este caso como acordamos no usar ninguna base de datos en especifico para no complicar el tutorial, usaremos un array normal como sustituto de una base de datos, el codigo dentro del callback quedaria asi:

//auth.js

// Este array imitara una tabla de la base de datos
const users = [];

passport.use(new FacebookTokenStrategy({
    clientID: 'INSERTA_TU_CLIENT_ID_AQUI',
    clientSecret: 'INSERTA_TU_CLIENT_SECRET_AQUI',
}, (accessToken, refreshToken, profile, cb) => {
    const user = users.find(user => user.facebookId === profile.id);

    if (user) {
        // El usuario existe asi que solo creamos y devolvemos un nuevo JWT
        const token = jwt.sign(user, 'jwt_secret');
        // Esto lo hacemos para tener el token en el mismo nivel de la informacion del usuario
        return cb(null, Object.assign({}, user, { token }));
    }

    // El usuario no existe por lo tanto crearemos un nuevo "registro" y le devolveremos un JWT para su nuevo usuario
    const newUser = {
        facebookId: profile.id,
        name: profile.displayName,
        email: profile.emails.length ? profile.emails[0].value : null,
    };

    users.push(newUser);

    const token = jwt.sign(newUser, 'jwt_secret');
    return cb(null, Object.assign({}, newUser, { token }));
}));

En el codigo anterior registramos nuestra nueva estrategia y dentro de su logica validamos si el usuario que pertenece al accessToken enviado existe o no, si no existe lo creamos y en ambos casos devolvemos la informacion del usuario junto con su nuevo JWT token.

Ahora para terminar de momento con este archivo, exportaremos el objeto passport con el siguiente codigo

module.exports = passport;


De momento el archivo auth.js deberia lucir asi

//auth.js

// Importamos las librerias que usaremos
const passport = require('passport');
const jwt = require('jsonwebtoken');
const FacebookTokenStrategy = require('passport-facebook-token');
const PassportJWT = require('passport-jwt');
const JWTStrategy = PassportJWT.Strategy;
const ExtractJwt = PassportJWT.ExtractJwt;



// Este array imitara una tabla de la base de datos
const users = [];

passport.use(new FacebookTokenStrategy({
    clientID: 'INSERTA_TU_CLIENT_ID_AQUI',
    clientSecret: 'INSERTA_TU_CLIENT_SECRET_AQUI',
}, (accessToken, refreshToken, profile, cb) => {
    const user = users.find(user => user.facebookId === profile.id);

    if (user) {
        // El usuario existe asi que solo le devolveremos un nuevo JWT
        const token = jwt.sign(user, 'jwt_secret');
        return cb(null, Object.assign({}, user, { token }));
    }

    // El usuario no existe por lo tanto crearemos un nuevo "registro" y le devolveremos un JWT para su nuevo usuario
    const newUser = {
        facebookId: profile.id,
        name: profile.displayName,
        email: profile.emails.length ? profile.emails[0].value : null,
    };

    users.push(newUser);

    const token = jwt.sign(newUser, 'jwt_secret');
    return cb(null, Object.assign({}, newUser, { token }));
}));

module.exports = passport;


Luego de esto, regresaremos a nuestro archivo app.js y initializaremos passport y agregaremos la nueva ruta que recibira el access token de Facebook desde nuestros clientes, recuerda que la ruta puede ser la que desees, en este caso sera /auth/facebook/token

//app.js

// Initializamos nuestro autenticacion con passport

app.use(passport.initialize());

app.get('/auth/facebook/token', passport.authenticate('facebook-token', { session: false }), (req, res) => {
    return res.json(req.user);
});

No olvides colocar el {session: false} en la ruta, ya que no queremos que passport cree una session para el usuario (El cual es el comportamiento por defecto).

Con el codigo anterior habra quedado configurada toda la logica que necesitamos para autenticar y registrar nuevos usuarios con Facebook, asi todo el archivo app.js quedaria asi

//app.js

// Codigo normal en cualquier API en express
const express = require('express');
const compression = require('compression');
const bodyParser = require('body-parser');

// Importamos las librerias que usaremos
const passport = require('passport');

const auth = require('./auth');

// Normal code
const app = express();

app.set("port", 3000)
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(passport.initialize());

app.get('/', (req, res) => {
    return res.send('Todo funcionando correctamente');
});

app.get('/auth/facebook/token', passport.authenticate('facebook-token', { session: false }), (req, res) => {
    return res.json(req.user);
});

app.listen(3000, () => {
    console.log('La app esta corriendo en dev');
});


Desde ahora cada vez que se llame a la ruta /auth/facebook/token con un accessToken valido se le devolvera al cliente un objeto con la siguiente estructura

{
"facebookId":"facebook id",
"name":"Miguel Crespo",
"email":"tu email",
"token":"jwt token"
}

El cliente debera almacenar el JWT token que le envio el servidor si quiere de ahora en adelante solicitar recursos protegidos.

Ahora crearemos dos nuevas rutas, una protegida y otra publica.

En app.js agregaremos la siguiente ruta publica:

//app.js

app.get('/home', (req, res) => {
    return res.send('Este es el home y es una ruta publica a la cual todos tienen acceso');
});

Y una ruta privada a la cual solo tendran acceso los usuarios autenticados.

//app.js

app.get('/profile', (req, res) => {
    return res.send('Esta es una ruta privada);
});

Configurando la estrategia de facebook-jwt

Antes de entrar a la ruta privada, primero deberemos configurar la estrategia de passport-jwt para proteger la ruta de usuarios sin autenticar, esto lo hacemos agregando el siguiente codigo en el archivo auth.js

//auth.js

passport.use(new JWTStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: 'jwt_secret'
}, (jwtPayload, cb) => {

    const user = users.find(user => user.facebookId === jwtPayload.facebookId);

    return cb(null, user);
}));
````

Con este simple bloque de codigo configuraramos la estrategia de passport-jwt, nota a que en este codigo le estamos indicando a la estrategia que el codigo lo debe buscar en el Header y tendra el formato de **Bearer .. Token ..**.

Claramente puedes configurar la estrategia para que tome el token de otros lugares, puedes encontrar todas las opciones disponibles en: https://github.com/themikenicholson/passport-jwt#extracting-the-jwt-from-the-request

El codigo final para el archivo **auth.js** sera el siguiente:

```javascript
//auth.js

// Importamos las librerias que usaremos
const passport = require('passport');
const jwt = require('jsonwebtoken');
const FacebookTokenStrategy = require('passport-facebook-token');
const PassportJWT = require('passport-jwt');
const JWTStrategy = PassportJWT.Strategy;
const ExtractJwt = PassportJWT.ExtractJwt;



// Este array imitara una tabla de la base de datos
const users = [];

passport.use(new FacebookTokenStrategy({
    clientID: 'INSERTA_TU_CLIENT_ID_AQUI',
    clientSecret: 'INSERTA_TU_CLIENT_SECRET_AQUI',
}, (accessToken, refreshToken, profile, cb) => {
    const user = users.find(user => user.facebookId === profile.id);

    if (user) {
        // El usuario existe asi que solo le devolveremos un nuevo JWT
        const token = jwt.sign(user, 'jwt_secret');
        return cb(null, Object.assign({}, user, { token }));
    }

    // El usuario no existe por lo tanto crearemos un nuevo "registro" y le devolveremos un JWT para su nuevo usuario
    const newUser = {
        facebookId: profile.id,
        name: profile.displayName,
        email: profile.emails.length ? profile.emails[0].value : null,
    };

    users.push(newUser);

    const token = jwt.sign(newUser, 'jwt_secret');
    return cb(null, Object.assign({}, newUser, { token }));
}));

passport.use(new JWTStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: 'jwt_secret'
}, (jwtPayload, cb) => {

    const user = users.find(user => user.facebookId === jwtPayload.facebookId);

    return cb(null, user);
}));

module.exports = passport;

Una vez configurado la estrategia, ahora podemos agregarla como un middleware a las rutas protegidas, de la siguiente forma:

//app.js

app.get('/profile', passport.authenticate('jwt', {session: false}), (req, res) => {
    return res.send('Esta es una ruta privada y tienes acceso porque estas autenticado como ' + req.user.name);
});


El codigo final de app.js deberia lucir como el siguiente

// Codigo normal en cualquier API en express
const express = require('express');
const compression = require('compression');
const bodyParser = require('body-parser');

// Importamos nuestro objeto de autenticacion
const passport = require('./auth');

// Normal code
const app = express();

app.set("port", 3000)
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(passport.initialize());

app.get('/', (req, res) => {
    return res.send('Todo funcionando correctamente');
});

app.get('/auth/facebook/token', passport.authenticate('facebook-token', { session: false }), (req, res) => {
    return res.json(req.user);
});

app.get('/home', (req, res) => {
    return res.send('Este es el home y es una ruta publica a la cual todos tienen acceso');
});

app.get('/profile', passport.authenticate('jwt', { session: false }), (req, res) => {
    return res.send('Esta es una ruta privada y tienes acceso porque estas autenticado como ' + req.user.name);
});

app.listen(3000, () => {
    console.log('The app is running on port 3000');
});

Despues de esto, si tratas de acceder a la ruta privada obtendras un 401 como error.

Screen-Shot-2018-05-13-at-5.12.05-PM

Esto significa que nuestra API ya esta completamente configurada para autenticar usuarios desde facebook y manejar rutas privadas.

Probando nuestra API

Para probar nuestra API usaremos una herramienta llamada Postman, puedes descargarla desde su pagina oficial Official Page

Y necesitaremos un accessToken de Facebook valido, como no queremos implementar ningun cliente, usaremos una herramienta que nos proporciona Facebook, llamada Graph API Explorer

https://developers.facebook.com/tools/explorer

developers.facebook.com_tools_explorer

  • Selecciona de la lista a la derecha la aplicacion con la que estas trabajando.
  • Luego de seleccionarla copia el valor que te sale en el input de Access Token.

Luego en postman crea un nuevo request, agregando tu access_token a la url en el siguiente formato

http://localhost:3000/auth/facebook/token?access_token=tu_access_token

Deberias obtener algo asi:

Screen-Shot-2018-05-13-at-5.21.55-PM

Luego copia el valor de token y creemos un nuevo request, esta vez a la ruta

http://localhost:3000/profile

Y agregaremos en la opcion de Headers, el header Authorization con el valor de Bearer …Tu token…

Y deberemos obtener un resultado asi:

Screen-Shot-2018-05-13-at-5.23.15-PM

Notas finales

  • Recuerda que todos los valores como jwt_secret, clientId, clientSecret deben ser tratados como contraseñas por lo tanto estos valores deberias configurar a traves de variables de entorno de node.

Te gusto el tutorial? Qué le cambiarias o le agregarías? Dejanos tus comentarios en la sección de comentarios que está abajo!

Passport, express, node