Ionic JWT auth with facebook using nodejs. Part 1

Recently on a work project I had to create JWT authentication with multiple OAuth providers and integrate it with Ionic app. While there are a lot of tutorials and guides online, I found some of them incomplete or addressing some other use cases.

In this 2 part blog post we are going to build a simple ionic app with express server to make the authentication and signing the JWT tokens. 

Part 2: http://blog.grossman.io/ionic-jwt-auth-with-facebook-using-nodejs-part-2-2/

Github for this project: https://github.com/scopsy/node-ionic-jwt

Why JWT tokens?

They are self-contained, which means that each token equipped with all the information needed for the authorization process including the expiration time and the issuer of the token. You can also attach additional payload data to the token, which can be decoded to a plain JS object on the client and back on your server. The payload can include user information such as his db ID, name, role and other useful information.

JWT tokens are multi-platform, we can use them on our ionic app, web-app and for api endpoints on the server. Since JWT transferred over JSON you can use them with multiple languages.

They are light-weight and can be attached to http headers easily.

I would suggest searching the web about JWT, there are a lot of great resources describing the details about them. We won’t go any further describing JWT since we just want to see them in action with our Ionic app.

Ok, so JWT are awesome! Let’s start coding.

Nodejs server

We will start with a simple express app that our client will be able to contact. In this project I will use ES6 syntax so grab the latest node version preferred 6.2.2.

package.json

{
  "name": "node-ionic-jwt-auth",
  "version": "0.0.1",
  "repository": {
    "type": "git",
    "url": "https://github.com/scopsy/node-ionic-jwt-auth.git"
  },
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "body-parser": "^1.15.2",
    "cookie-parser": "^1.4.3",
    "cors": "^2.7.1",
    "dotenv": "^2.0.0",
    "express": "^4.14.0",
    "jsonwebtoken": "^7.0.1",
    "mongoose": "^4.5.1",
    "morgan": "^1.7.0",
    "request": "^2.72.0"
  },
  "license": "MIT"
}

Open your terminal and type “npm install”, while npm does it’s magic go grab a cup of coffee.

// server/app.js
/**
 * Module dependencies.
 */
const express       = require('express');  
const bodyParser    = require('body-parser');  
const logger        = require('morgan');  
const dotenv        = require('dotenv');  
const path          = require('path');  
const mongoose      = require('mongoose');  
const cors          = require('cors');

/**
 * Load environment variables from .env file, where API keys and passwords are configured.
 */
dotenv.load({ path: '.env' });

/**
 * Load app modules and routes
 */
const AuthModule    = require('./config/AuthModule');  
const TokenService  = require('./config/TokenService');  
const authCtrl      = require('./controllers/auth.ctrl');

/**
 * Create Express server.
 */
const app = express();

/**
 * Connect to MongoDB.
 */
mongoose.connect(process.env.MONGODB_URI);  
mongoose.connection.on('error', () => {  
    console.error('MongoDB Connection Error.');
    process.exit(1);
});

/**
 * Express configuration.
 */
app.set('port', process.env.PORT || 3000);  
app.use(logger('dev'));  
app.use(cors({  
    origin: '*',
    withCredentials: false,
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin' ]
}));

/**
 * Init body and cookie inside req
 **/
app.use(bodyParser.json());  
app.use(bodyParser.urlencoded({extended: true}));

/**
 * Token deserialization, 
 * here we will check for token existence and extract the user payload
 * We will use the payload on other routes down the request pipeline
 */
app.use((req, res, next) => {  
    const token = new TokenService(req.headers);

    req.isAuthenticated = token.isAuthenticated;
    req.tokenPayload    = token.getPayload();
    req.user            = {
        _id: req.tokenPayload._id
    };

    next();
});

app.use(express.static(path.join(__dirname, 'public')));


/**
 * Endpoints.
 */
app.post('/auth/facebook',  
    authCtrl.facebookAuth, authCtrl.retrieveUser, authCtrl.generateToken, (req, res) => {
    res.json({token: req.genertedTokenn});
});

/**
 * Start Express server.
 */
app.listen(app.get('port'), () => {  
    console.log(`Express server listening on port ${app.get('port')} in ${app.get('env')} mode`);
});

module.exports = app;

So our app.js file contains all necessary configuration and boilerplate. Note that we require a few files to our main file. Let's start examine those.

.env file

This file is containing all server configurations, API KEYS, and other sensitive data. It is usually a good practice not to upload this file to the git repository and place the .env in a .gitignore file. You can allways use .env.example as a blueprint file for your teammates.

// .env
MONGODB_URI=mongodb://localhost:27017/testdb  
TOKEN_SECRET=supersecrettoken

FACEBOOK_ID=yourfbid  
FACEBOOK_SECRET=yourfbsecret  
AuthModule.js

This file will contain all of our authentication logic, we don't want it to be coupled to our routes, so will extract all of it to a different module.

In this module we will export 2 methods, facebookAuthentication and createOrRetrieveUser.

facebookAuthentication

Will be responsible for converting our Authorization code acquired from our ionic app. We will send a GET request to the facebook api to exchange our auth code to an accessToken which will allow as later to get the users profile information. We will want to normalize facebook profile object to user object similiar to our UserSchema, so later we will be able use different providers, and reuse other parts of our authorization process.

In order to communicate with facebook api you will need to obtain a FACEBOOK_SECRET key. To do so you will have to create an app on facebook developers panel.

Let's get our keys!

  • Login with your account to Facebook Developers
  • Click on My Apps > Add a New App in the nav bar.
  • Select your app category(WWW)
  • Enter your apps name
  • Click on the Settings tab and click Add Platform
  • Choose WWW and enter http://localhost:3000
  • Now on the left panel under products click Add Product
  • Select Facebook Login and click Get Started
  • Make sure Client Oauth Login and Web Oauth Login enabled
  • Under Valid OAuth redirect URIs enter: http://localhost:8100 (ionic app viewer), http://localhost:3000 (for your server) (the ports may vary depending on your setup)
  • And finally Save Changes
  • Go grab your app id and secret key from settings page and put them inside your .env file. We will use the app id later with our ionic app.
createOrRetrieveUser

Will simply search the users collection for a user matching profile id based on the authorization type. in our case will search for the facebook key on our Schema. Later we can add additional providers as well.

Here is the final code for the AuthModule.js file.

// config/AuthModule.js
'use strict';  
const request   = require('request');  
const User      = require('../models/User');

module.exports = {  
    facebookAuthentication,
    createOrRetrieveUser
};

function facebookAuthentication(options, cb) {  
    const fields          = ['id', 'email', 'first_name', 'last_name', 'link', 'name'];
    const accessTokenUrl  = 'https://graph.facebook.com/v2.5/oauth/access_token';
    const graphApiUrl     = `https://graph.facebook.com/v2.5/me?fields=${fields.join(',')}`;

    const params = {
        code: options.code,
        client_id: options.clientId,
        redirect_uri: options.redirectUri,
        client_secret: process.env.FACEBOOK_SECRET
    };

    // Step 1. Exchange authorization code for access token.
    request.get({url: accessTokenUrl, qs: params, json: true}, (err, response, accessToken) => {
        if(response.statusCode !== 200) return cb(accessToken.error.message);

        // Step 2. Retrieve profile information about the current user.
        request.get({url: graphApiUrl, qs: accessToken, json: true}, (err, response, profile) => {
            if(response.statusCode !== 200) return cb(accessToken.error.message);

            // Here we will normalize facebook response to our user schema
            // So later we can use multiple providers
            const user = {
                profilePicture: `https://graph.facebook.com/${profile.id}/picture?type=large`,
                firstName: profile.first_name,
                lastName: profile.last_name,
                facebook: profile.id,
                email: profile.email,
                token: accessToken
            };

            cb(null, {type:'facebook', user});
        });
    });
}

function createOrRetrieveUser(options, cb) {  
    // select the query object based on the auth type
     const query = {
        [`profiles.${options.type}`]: options.user.profiles[options.type]
    };

    User.findOne(query, (err, user) => {
        if(err) return cb('Error fetching user');

        // User found, return him to the callback
        if(user) return cb(null, user);

        // No user is found, create new user
        createUser(options.user, cb);
    });
}

function createUser(user, cb) {  
    const newUser = new User(user);

    newUser.save(cb);
}
TokenService.js

This class will be responsible for our JWT creation and validation. We will expose createToken static method for generating our tokens.

Here is the code for TokenService.js:

// config/TokenService.js
const jwt = require('jsonwebtoken');

class TokenService {  
    constructor(headers) {
        this.token      = this._extractTokenFromHeaders(headers);
        this.payload    = {};
        this.validToken = false;

        this._verifyToken();
    }

    static createToken(options, cb) {
        const payload = {
            profilePicture: options.user.profilePicture,
            firstName: options.user.firstName,
            lastName: options.user.lastName,
            _id: options.user._id
        };

        jwt.sign(payload, process.env.TOKEN_SECRET, {
            algorithm: 'HS256',
            expiresIn: options.expireTime || 1440 // expires in 24 hours
        }, cb);
    }

    getPayload() {
        return this.payload;
    }

    isAuthenticated() {
        return this.validToken;
    }

    _verifyToken() {
        if(!this.token) return;

        try {
            this.payload    = jwt.verify(this.token, process.env.TOKEN_SECRET);
            this.validToken = true;
        } catch (err) {
            this.payload    = {};
            this.validToken = false;
            console.log(err);
        }
    }

    _extractTokenFromHeaders(headers) {
        if(!headers || !headers.authorization) return false;

        return headers.authorization.replace('Bearer ', '');
    }
}

module.exports = TokenService;  
User Schema

Our user model will be pretty straight forward:

// models/User.js

const mongoose  = require('mongoose');

const userSchema = new mongoose.Schema({  
    firstName: String,
    lastName: String,
    email: { type: String, unique: true },
    profilePicture: String,

    profiles: {
        facebook: String,
        google: String
    },
    tokens: Array
}, { timestamps: true });


module.exports = mongoose.model('User', userSchema);  

After we finished with our services and models we can combine it all together with an auth controller

Authentication Controller

This module will interact with our express routes, so we will import here a few methods that later will be used as reusable middlewares for other providers.

// controllers/auth.ctrl.js

const AuthModule   = require('../config/AuthModule');  
const TokenService = require('../config/TokenService');

module.exports = {  
    facebookAuth,
    retrieveUser,
    generateToken
};

function facebookAuth(req, res, next) {  
    const options = {
        code: req.body.code,
        clientId: req.body.clientId,
        redirectUri: req.body.redirectUri
    };

    AuthModule.facebookAuthentication(options, (err, response) => {
        if(err) return res.status(401).json({err: 'Error during facebook oauth'});

        // for larger apps recommended to namespace req variables
        req.authObject = response;

        next();
    });
}

// Here we will generate the user or retrieve existing one to pass for our token generator
function retrieveUser(req, res, next) {  
    if(!req.authObject) return res.status(401).json({err: "Error while fetching user"});

    const userToRetrieve = {
        user: req.authObject.user,
        type: req.authObject.type
    };

   AuthModule.createOrRetrieveUser(userToRetrieve, (err, user) => {
        if(err || !user) return res.status(401).json({err: 'Error while fetching user'});

        req.user = user;

        next();
    });
}

// The last Middleware in the chain
// reponsible for returning the generated token back to client
function generateToken(req, res, next) {  
    TokenService.createToken({user: req.user}, (err, token) => {
        if(err) return next({status: 401, err: 'User Validation failed'});

        req.genertedToken = token;

        next();
    });
}

After creating the controller and our auth module we can import the controller to our app.js file :

const AuthModule = require('./config/AuthModule');  
const authCtrl   = require('./controllers/auth.ctrl');  

We will make use of expressjs middlewares to use with our authentication routes. Express middlewares are simply functions that inserted inside the request pipeline to add functionality or process the request before responding to the client.

We will create our auth route with multiple middlewares, each of them will be responsible for its own part. So later when we will introduce more authentication methods we will be able to reuse most of our code.

app.post('/auth/facebook',  
    authCtrl.facebookAuth, authCtrl.retrieveUser, authCtrl.generateToken, (req, res) => {
    res.json({token: req.genertedTokenn});
});

Note that the request is passed inside of all the controller methods in the exact order:

  1. We exchange our authorization token with an access token, and then fetch the user profile in a normalized object.
  2. With the user object passed along with the auth provider from the previous middleware we will search for the user inside the DB. If found, return the user to the next step, if not create the user and then return him to next step.
  3. Generate the token including user info payload.
  4. Finally we will send the token back to the user as json

Protected routes

Later when we want to check if the user is authenticated in order to access specific endpoint we can write simple middleware:

function isAuthenticated(req, res, next) {  
    if(req.isAuthenticated()) return next();

    if(req.xhr) {
        return res.status(401).json({err: 'Unauthorized'});
    } else { 
       // You can redirect to login page here aswell
       return res.status(401).send('Unauthorized');
    }
}

app.get('/api/protectedRoute' isAuthenticated, routeHandler)  

So now we finished setting up our server, we can simply enter the server folder and run npm start from CLI.

If everything is good you will see some TypeError exceptions and the server will probably won't start :) after fixing them you will see:
Express server listening on port 3000 in development mode

So our awesome node server is running, time to take care of our Ionic app in the next part of this blog post.

Dima Grossman

Read more posts by this author.

Subscribe to Dima's code blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!
comments powered by Disqus