Hoy en dia es comun ver que en una variedad de aplicaciones web y móvil se presenta la opción de “iniciar sesión con facebook”, lo cual resulta conveniente ya que los usuarios no tendrán que escribir su username y su password para iniciar sesión, si no que bastará con el toque de un botón.

Existen herramientas construidas alrededor de esta idea como passportjs que buscan simplificar el proceso. Pero el objetivo de este artículo es explicar cómo lo podemos lograr sin agregar dependencias a nuestro proyecto.

Para explicar el flujo de autenticación (oAuth2 grant type authorization code) usado por facebook he creado el siguiente diagrama de flujo:

 

 

A primera vista parece mucho, pero realmente solo son 6 sencillos pasos. Siempre hay que tomar en cuenta que simplificarle la experiencia al usuario require trabajo.

Lo primero que debemos hacer es colocar el boton de facebook en algun lugar, para ello insertamos el siguiente codigo html en la vista de nuestra aplicacion.

<div class="fb-login-button"
    data-max-rows="1"
    data-size="large"
    data-button-type="continue_with"
    data-show-faces="false"
    data-auto-logout-link="false"
    data-use-continue-as="false"
    scope="public_profile,email"
    onlogin="checkLoginState();">
</div>

Este es el formato que mas tarde el sdk de facebook reconocera y reemplazara esto por un boton con su propio branding segun las especificaciones que le dimos.

Ahora agregamos un archivo js donde ira nuestro codigo que nombraremos “auth.js” para efectos de ejemplo, pero que ustedes pueden llamar como mejor les parezca. Y ademas, por simplicidad, agregare jQuery, pero ustedes, como dije antes, pueden usar el framework de frontend que mejor les parezca.

<script src="https://code.jquery.com/jquery-migrate-1.4.1.min.js"></script>
<script src="auth.js"></script>

Ahora bien, en nuestro auth.js vamos a agregar la salsa secreta para el facebook login, que consiste en su forma asincrona de cargar el sdk y de la funcion FB.init, adicionalmente el código de la funcion checkLoginState del evento onLogin que pusimos en nuestro html

$(document).ready(() => {
    window.fbAsyncInit = () => {
        FB.init({
            appId: 'Tu App ID aqui',
            cookie: true,
            xfbml: true,
            version: 'v2.9',
        });
        FB.AppEvents.logPageView();
        window.checkLoginState = () => {
            FB.getLoginStatus((response) => {
                if (response.status === 'connected') {
                    // El usuario autorizo nuestra app.
                    startToAuth(response);
                } else {
                    // El usuario rechazo nuestra app.
                }
            });
        };
    };
    (function (d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) {
            return;
        }
        js = d.createElement(s);
        js.id = id;
        js.src = '//connect.facebook.net/en_US/sdk.js';
        fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));
});

Hay que notar 2 coasas en este código:

  1. Tienes que gestionar un app ID en tu cuenta de facebook. Para ello puedes referirte a la documentacion de facebook sobre como hacerlo, es super facil y no toma mucho tiempo.
  2. Las lineas comentadas representan la condición del diagrama de flujo antes ilustrado, en donde el usuario autorizo o no nuestra app. En esta seccion tendras que agregar el código que maneja tu lógica de post-autenticacion o bien como manejamos si el usuario rechaza nuestra app.

Si nos concentramos en la parte en la que el usuario felizmente acepto nuestra app, queda intercambiar el authorization code, por un access token que nos servira para traer los datos del usuario. Para ello tenemos la implementación del metodo startToAuth

function startToAuth(response) {
    $.ajax({
        type: 'POST',
        url: '/api/auth/facebook',
        data: response,
        success: (data, status, xhr) => {
            // Aqui devuelve el access token y la data del usuario que queremos manejar!
        },
        error: (xhr, errorType, error) => {
            // Oops! algo fallo y debemos manejarlo aqui
        },
    });
}

Lo anterior no es mas que una simple llamada en ajax a nuestro propio backend para que intercambie el access token por la informacion del usuario.

Para estos efectos usare expressjs como backend, con el siguiente archivo que maneja la ruta /api/auth/facebook de nuestra aplicacion y corre en el puerto 3000

var express = require('express');
var app = express();
var axios = require('axios');

const asyncAllFBAuth = (req, res, next) => {
    const queue = [
        fbAuth.exchangeAccessToken(req),
        fbAuth.retrieveUserInfo(req),
    ];
    Promise.all(queue).then((values) => {
        res.locals.auth = values[0];
        res.locals.user = values[1];
        next();
    }).catch((err) => {
        next(err)
    });
};

const exchangeAccessToken = (req) => {
    return axios.get('https://graph.facebook.com/oauth/access_token?', {
        params: {
            grant_type: 'fb_exchange_token',
            client_id: config.facebookAuth.appID,
            client_secret: config.facebookAuth.appSecret,
            fb_exchange_token: req.body.authResponse.accessToken,
        },
    }).then((response) => {
        if (response.status === 200 && !response.data.error) {
            return response.data;
        }
    });
};

const retrieveUserInfo = (req) => {
    const userID = req.body.authResponse.userID;
    return axios.get(`https://graph.facebook.com/${config.facebookAuth.appVer}/${userID}?`, {
        params: {
            access_token: req.body.authResponse.accessToken,
            fields: 'id,name,short_name,name_format,first_name,middle_name,last_name,gender,email,verified,is_verified,cover,picture,timezone,currency,locale,age_range,updated_time,link,devices,is_shared_login,can_review_measurement_request',
        },
    }).then((response) => {
        if (response.status === 200 && !response.data.error) {
            return response.data;
        }
    });
};

app.use(asyncAllFBAuth);

app.post('/api/auth/facebook', function (req, res) {
	res.send(res.locals);
});

app.listen(3000, function () {
  console.log('El Backend de nuestra app esta corriendo!');
});

Este codigo puede ser abrumador al principio, pero tratare de explicarlo:

El controlador hace uso de un middleware, un middleware es un mecanismo para que cada peticion al backend ejecute estas instrucciones antes de lo que esta en el manejador de la ruta. Esto quiere decir que por cada app.post que se haga a este backend tendra que hacerse primero la autenticación por facebook que consta de dos partes, intercambiar el authorization code por el access token (exchangeAccessToken), y luego el access token por la data del usuario (retrieveUserInfo).

Este proceso se realiza de manera sincrona gracias a Promise.all que acepta un array de funciones y resuelve esas promesas en orden y copia los resultados convenientemente en un array. Toda esta logica esta en asyncAllFBAuth.

Si quisieramos opcionalmente gestionar un jwt para dicho usuario podriamos crear otro middleware como este:

const issueJWT = (req, res, next) => {
    const signUser = {
        id: res.locals.user._id.toString(),
        screen_name: res.locals.user.name.screen_name,
        picture: res.locals.user.picture.url,
        iss: 'projectTalk',
    };
    const expireTime = res.locals.token_expires_in || config.tokenExpired;
    jwt.sign(signUser, config.HS256, { expiresIn: expireTime, algorithm: 'HS256' },
        (err, token) => {
            if (err) {
                next(err);
                return;
            }
            res.locals.token = token;
            next();
        });
};

O poner esta misma logica en otro endpoint sin los next()…  cuestion de gustos 🙂

Si regresamos a nuestra llamada ajax con jQuery en el frontend, podemos ahora manejar los datos que nos devuelve este endpoint, y hacer lo que necesitemos con la informacion del usuario.

 

Tagged in:

, , , ,