Tutorial hero
Lesson icon

Creating Role Based Authentication with Passport in Ionic 2 – Part 1

Originally published October 07, 2016 Time 27 mins

There many ways we can authenticate users in Ionic 2 applications and most of them are quite simple to implement. We could use Ionic Auth, social logins (Facebook, Google, Twitter), Firebase, SuperLogin, your own custom authentication on your own server and many more. If all we need to do for authentication is to grant a user access to the application and assign them some identifier to keep track of their information, there’s not too much we need to worry about.

It gets a little more complicated when we need to restrict access to certain functions or data based on a user’s role. We might want to have different types of users – like normal users, moderators, and admins for example – that all have access to different things.

In this tutorial, I am going to show you how to create a flexible, role-based authentication system using Passport and the MEAN stack (MongoDB, Express, Angular – or in our case, Ionic 2 -, and Node). This combination works really well because Passport will allow you to authenticate with over 300 different providers, and we can create easily restrict access to certain routes based on a user’s role on the Express server.

We will be creating a server and a front end in Ionic 2 to create a “Shared Todo” application. Users will be able to sign up and login in to gain access to a shared list of todos. What permissions the user has in the application will depend on their role. We will create a reader role that can only read the todos, a creator role that can read the todos and create new ones, and an editor role that can create, read, and delete todos.

I have created a tutorial using the MEAN stack before, but this time the server is going to be a little more complex. Before reading you should at least a basic understanding of Node, or some exposure to it, but I will be taking time to explain things in quite a lot of detail. We will also be making use of JSON Web Tokens (JWT) for authentication, so if you are unfamiliar with JWTs I would recommend reading one of my previous JWT tutorials. Without understanding how a JWT works and why it can be used for authentication, some of this might just seem like magic. You might also be lead to the false assumption that the contents of a JWT are secure, and use them to communicate secure data, they however are definitely not able to be used for that.

The structure of the server we will be creating is heavily based on Refactoring a Basic, Authenticated API with Node, Express, and Mongo by Joshua Slate, so a huge thanks to him for his work on that.

Before we Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine. You should also have a basic understanding of Node, Express, and MongoDB. For a basic introduction to NoSQL and MongoDB you can read An Introduction to NoSQL for HTML5 Mobile Developers. Node and Express are explained in a reasonable amount of detail in this tutorial, so additional reading may not be required.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

1. Create the Folder and File Structure

We will be creating two different projects for this tutorial, one for our Ionic 2 front end (the client) and one for the Node backend (the server). This tutorial is going to focus on the server, and we aren’t going to use any tools to auto-generate the project for us like the Ionic CLI will do for our Ionic project, so we are just going to create the structure manually.

Create the file and folder structure shown below:

Server Folder Structure

Notice that the image above also includes a client folder, this is where we will eventually create our Ionic 2 project, but for now we will just be focusing on the server folder.

For now, just create empty files for everything shown above, we will go through adding code to them and explaining what they are for in the next sections.

2. Set up the Project and Dependencies

We are going to set up our package.json file first, which will allow us to install all of the dependencies for our application, and it also describes how we want to run the server. When we deploy this server to Heroku, since we have the start property set to node server.js, Heroku will know to start the server by invoking the server.js file. If you were to run this server locally using Node, you would just type node server.js in the terminal to start the server.

Modify package.json to reflect the following:

{
  "name": "ionic2-roles",
  "version": "0.1.0",
  "description": "A sample Node.js app using Express 4",
  "engines": {
    "node": "5.9.1"
  },
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "bcrypt-nodejs": "0.0.3",
    "body-parser": "^1.15.2",
    "cors": "^2.8.1",
    "express": "^4.14.0",
    "jsonwebtoken": "^7.1.9",
    "mongoose": "^4.6.3",
    "morgan": "^1.7.0",
    "passport": "^0.3.2",
    "passport-jwt": "^2.1.0",
    "passport-local": "^1.0.0"
  }
}

If you want to run this server locally, you should also run npm install after setting up this file, but if you are deploying to Heroku then you don’t need to worry about installing the dependencies locally.

There are a few dependencies here, so let’s talk through what each of them does:

  • bcrypt-nodejs allows us to use a hashing algorithm to secure passwords stored in our database
  • body-parser is middleware for our express server, that allows us to parse request bodies
  • cors will deal with any Cross Origin Resource Sharing (CORS) issues we might run into
  • express is of course the express server itself, which is a simple server framework for Node
  • jsonwebtoken allows us to create, sign, and read JSON Web Tokens
  • mongoose allows us to easily work with objects in our MongoDB database
  • morgan outputs some useful debugging information for us
  • passport is our authentication middleware
  • passport-jwt is one of Passport’s authentication “Strategies” which are like plugins
  • passport-local is another Passport Strategy, which uses simple username and password authentication

It’s also worth mentioning that there are literally 100’s of Strategies for Passport, including Google OAuth, Fitbit, Facebook, HTTP Bearer, and obviously many more. You can use any number of these in your application, we will just be using jwt and local though. The JWT strategy will allow us to automatically authenticate users who possess a valid JWT, and the Local strategy will allow us to log users in using their email address and password (at which point, we will create a new JWT for them).

Now let’s get into creating the rest of the server files. We are going to add the complete code for each file all at once, and explain them as we go. This means we are going to be making references to things we haven’t actually created yet, but the tutorial would be far too long if we were to do it step-by-step.

3. Create the Main Server File

The server.js file is responsible for setting everything in the server up, because it is the first file that will be executed, so let’s set that up now.

Modify server.js to reflect the following

var express = require('express');
var app = express();
var mongoose = require('mongoose');
var logger = require('morgan');
var bodyParser = require('body-parser');
var cors = require('cors');

var databaseConfig = require('./config/database');
var router = require('./app/routes');

mongoose.connect(databaseConfig.url);

app.listen(process.env.PORT || 8080);
console.log('App listening on port 8080');

app.use(bodyParser.urlencoded({ extended: false })); // Parses urlencoded bodies
app.use(bodyParser.json()); // Send JSON responses
app.use(logger('dev')); // Log requests to API using morgan
app.use(cors());

router(app);

In the code above we are using express to generate a new server, and we assign a reference to that server object to app. We import our configuration options for the database and the routes for our application which we will set up later.

We then connect to our MongoDB database by using mongoose.connect. We will specify the link to this database later in the config file. Once we have connected to the database, we set up our server to listen on either process.env.PORT or port 8080. In order for the server to work with Heroku, we must specify process.env.PORT but by also adding a second option of 8080 it will also allow you to run the server locally with Node.

Next, we set up our servers “middleware” using the app.use method. Here we set up bodyParser and logging with morgan which we installed as dependencies earlier. We also add the cors package so that we don’t get CORS errors when running the application locally.

Finally, we make the following call:

router(app);

which sets up the routes for our application (based on the routes we imported earlier as router). The routes are an important part of the server, and we will get to those eventually, but first let’s set up all of our configuration files.

4. Set up the Config Files

We have a few configuration files in the project that will be used throughout the server, let’s set those up now.

Modify config/auth.js to reflect the following:

module.exports = {
  secret: 'eypZAZy0CY^g9%KreypZAZy0CY^g9%Kr',
};

Here we simply define the ‘secret’ or ‘key’ that will be used to sign our JSON Web Tokens. It’s important that this value is kept private, because if it is compromised it means a malicious user could generate valid JSON Web Tokens for your server containing whatever they want.

Also, it’s worth noting that we use module.exports here. A Module is basically just a way to bundle up related code together, kind of like creating a class. This will allow us to import the code contained within this module elsewhere by using require (like we are already doing in server.js for the database config and routes).

The same concept is used for importing things in our Ionic 2 applications, i.e.:

import { Something } from 'somewhere';

That is just using newer ES6 syntax, instead of using require.

Modify config/database.js to reflect the following:

module.exports = {
  url: 'mongodb://<username>:<password>@ds123456.mlab.com:49466/mongodbroles',
};

We’re using the same concept here to define the URL for our MongoDB database. You can use any MongoDB database you like, whether that is a local version or a DBaaS solution like mLab that I am using here. mLab allows you to create free MongoDB databases in a development environment, so if you’re unsure of what to use just go with that, and replace the URL above with a link to your own database (also make sure to create a user on the database, and add the credentials for that user above).

MongoDB also offers the MongoDB Atlas Cloud Service, but we won’t be working with that here.

Modify config/passport.js to reflect the following:

var passport = require('passport');
var User = require('../app/models/user');
var config = require('./auth');
var JwtStrategy = require('passport-jwt').Strategy;
var ExtractJwt = require('passport-jwt').ExtractJwt;
var LocalStrategy = require('passport-local').Strategy;

var localOptions = {
  usernameField: 'email',
};

var localLogin = new LocalStrategy(localOptions, function (
  email,
  password,
  done
) {
  User.findOne(
    {
      email: email,
    },
    function (err, user) {
      if (err) {
        return done(err);
      }

      if (!user) {
        return done(null, false, {
          error: 'Login failed. Please try again.',
        });
      }

      user.comparePassword(password, function (err, isMatch) {
        if (err) {
          return done(err);
        }

        if (!isMatch) {
          return done(null, false, {
            error: 'Login failed. Please try again.',
          });
        }

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

var jwtOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeader(),
  secretOrKey: config.secret,
};

var jwtLogin = new JwtStrategy(jwtOptions, function (payload, done) {
  User.findById(payload._id, function (err, user) {
    if (err) {
      return done(err, false);
    }

    if (user) {
      done(null, user);
    } else {
      done(null, false);
    }
  });
});

passport.use(jwtLogin);
passport.use(localLogin);

Now we’re getting into the more interesting stuff. This file is responsible for configuring Passport with the JWT and Local login strategies that we want to use.

First, we import everything we need using require. This includes the relevant strategies from Passport, but also the auth configuration we just set up and our User model (which we will set up next). We will go into more detail about the user model next, but basically the model defines the structure of the data for our user object which we will store in the database (we will also have a model for our Todo object later) and it also allows us to define some functions that we can use with the model.

Next we create our localLogin strategy and supply it some options so that we can use the email field instead of a username field. We use the findOne method that Mongoose provides to find a user object that matches the supplied email. If a user can not be found, or if there is an error, we return the error.

If we do find a user matching that email, we call the comparePassword function (which we will create on the user model in a moment), to check if the supplied password matches the stored password. If the password is wrong, we return an error, if not the strategy successfully completes.

The JWT strategy is similar, but a lot simpler. We extract the JWT sent with the request, and use the secret supplied by our auth configuration to check its validity. We then use the _id from that JWT to check if there are any matching users in our database with that id, and if there is the strategy will succeed.

If you want a little more detail on how exactly JWT works for authentication, I’d again recommend that you read Using JSON Web Tokens (JWT) for Custom Authentication in Ionic 2.

Finally, we set those two strategies on Passport by using passport.use().

5. Set up the Models

Mongoose allows us to create models for the data in our applications that are based on a Schema. This just describes what the data object should look like, and it allows us to easier store and retrieve the objects from our MongoDB database.

Modify models/user.js to reflect the following:

var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');

var UserSchema = new mongoose.Schema(
  {
    email: {
      type: String,
      lowercase: true,
      unique: true,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
    role: {
      type: String,
      enum: ['reader', 'creator', 'editor'],
      default: 'reader',
    },
  },
  {
    timestamps: true,
  }
);

UserSchema.pre('save', function (next) {
  var user = this;
  var SALT_FACTOR = 5;

  if (!user.isModified('password')) {
    return next();
  }

  bcrypt.genSalt(SALT_FACTOR, function (err, salt) {
    if (err) {
      return next(err);
    }

    bcrypt.hash(user.password, salt, null, function (err, hash) {
      if (err) {
        return next(err);
      }

      user.password = hash;
      next();
    });
  });
});

UserSchema.methods.comparePassword = function (passwordAttempt, cb) {
  bcrypt.compare(passwordAttempt, this.password, function (err, isMatch) {
    if (err) {
      return cb(err);
    } else {
      cb(null, isMatch);
    }
  });
};

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

This file sets up our User model for us. We use mongoose.Schema to define the structure of the model. We add email, password, and role fields to the model, so a user object might end up looking something like this:

{
    email: 'test@test.com',
    password: 'eroihf0832h0832',
    role: 'reader'
}

We also add timestamps to the model though, so the actual object that is created and stored in the database will look something like this:

{
    "_id": {
        "$oid": "57f5228e63ff270011ee782c"
    },
    "updatedAt": {
        "$date": "'2016-10-05'T15:55:58.828Z"
    },
    "createdAt": {
        "$date": "'2016-10-05'T15:55:58.828Z"
    },
    "email": "test@test.com",
    "password": "$2a$05$J/o2ACf5WlTgshmlD9GxdOfVuanRPdqjpHkVBTvZSQ/HJkwTCeKue",
    "role": "creator",
    "__v": 0
}

(this example has been pulled directly out of the mLab database for the project)

The most important part of this model is the function we add using the pre hook, which will run before the object is saved to the database. This function takes care of hashing the user’s password using bcrypt before storing it and to understand why that is important we should talk a little bit about password security and the importance of hashing.

Hashing is similar to encryption, but it is not the same thing. Encryption is two-way, meaning that we can encrypt a message, and then decrypt it later to reveal the original message. Hashing is one way, so once a value is hashed there is no way to retrieve the original value (assuming that the hashing algorithm is good).

This is important because this means that if your database is compromised (and you should assume that it will be, even if it is likely that it won’t), and hackers were to obtain all of your user’s passwords, it would be mostly worthless to them because all of the passwords would be hashed. This also means that you, as the application owner, also have no way of knowing what the users password actually is.

Instead, we can simply check if a users password is correct by hashing the password they try to log in with, and compare it to the hashed version of their password we have stored. We don’t know what the original password is, but we know if the hashes match then the user must have used the same password that they did originally.

The reason this is so important is that many people use the same password in a lot of places (even though they shouldn’t), and if you are incorrectly storing passwords, you are creating avenues for hackers to attack users other accounts that use the same password.

We also define a comparePassword function on the user model, which is used for the purpose of comparing the password hash we have stored, to the hashed version of the password they are trying to log in with.

Modify models/todo.js to reflect the following:

var mongoose = require('mongoose');

var TodoSchema = new mongoose.Schema(
  {
    title: {
      type: String,
      required: true,
    },
  },
  {
    timestamps: true,
  }
);

module.exports = mongoose.model('Todo', TodoSchema);

After the user mode, this doesn’t really need any explaining. We are just creating a new schema for our Todo model that contains a single ‘title’ field.

6. Set up the Controllers

Now we are going to define our controllers. Our controllers are responsible for controlling what happens when a user hits a route. Essentially, in our routes file we will define the routes, and then link to the controller for the behaviour we want to execute when that route is accessed.

Modify controllers/todos.js to reflect the following:

var Todo = require('../models/todo');

exports.getTodos = function (req, res, next) {
  Todo.find(function (err, todos) {
    if (err) {
      res.send(err);
    }

    res.json(todos);
  });
};

exports.createTodo = function (req, res, next) {
  Todo.create(
    {
      title: req.body.title,
    },
    function (err, todo) {
      if (err) {
        res.send(err);
      }

      Todo.find(function (err, todos) {
        if (err) {
          res.send(err);
        }

        res.json(todos);
      });
    }
  );
};

exports.deleteTodo = function (req, res, next) {
  Todo.remove(
    {
      _id: req.params.todo_id,
    },
    function (err, todo) {
      res.json(todo);
    }
  );
};

First, we import our Todo model so that we can make use of it in this controller, then we define three functions as exports (which is essentially the same as using module.exports).

We have a getTodos function that will return all of the todos stored in the database, a createTodo function that will insert a new todo into the database and then return all of the todos that are now in the database, and a deleteTodo function that will remove a specific todo from the database.

Modify controllers/authentication.js to reflect the following:

var jwt = require('jsonwebtoken');
var User = require('../models/user');
var authConfig = require('../../config/auth');

function generateToken(user) {
  return jwt.sign(user, authConfig.secret, {
    expiresIn: 10080,
  });
}

function setUserInfo(request) {
  return {
    _id: request._id,
    email: request.email,
    role: request.role,
  };
}

exports.login = function (req, res, next) {
  var userInfo = setUserInfo(req.user);

  res.status(200).json({
    token: 'JWT ' + generateToken(userInfo),
    user: userInfo,
  });
};

exports.register = function (req, res, next) {
  var email = req.body.email;
  var password = req.body.password;
  var role = req.body.role;

  if (!email) {
    return res.status(422).send({ error: 'You must enter an email address' });
  }

  if (!password) {
    return res.status(422).send({ error: 'You must enter a password' });
  }

  User.findOne({ email: email }, function (err, existingUser) {
    if (err) {
      return next(err);
    }

    if (existingUser) {
      return res
        .status(422)
        .send({ error: 'That email address is already in use' });
    }

    var user = new User({
      email: email,
      password: password,
      role: role,
    });

    user.save(function (err, user) {
      if (err) {
        return next(err);
      }

      var userInfo = setUserInfo(user);

      res.status(201).json({
        token: 'JWT ' + generateToken(userInfo),
        user: userInfo,
      });
    });
  });
};

exports.roleAuthorization = function (roles) {
  return function (req, res, next) {
    var user = req.user;

    User.findById(user._id, function (err, foundUser) {
      if (err) {
        res.status(422).json({ error: 'No user found.' });
        return next(err);
      }

      if (roles.indexOf(foundUser.role) > -1) {
        return next();
      }

      res
        .status(401)
        .json({ error: 'You are not authorized to view this content' });
      return next('Unauthorized');
    });
  };
};

This one is a little bit more complicated, but it’s not too bad. First, we define a couple of functions that will be used internally in this file. The generateToken function which will generate a JWT for a user, and a setUserInfo function that handles setting only the required information for the JWT. We do not want to send the users password or any other sensitive information in the JWT – by setting the users id in the JWT we know who they are and what they should have access to, but if a nefarious user was able to see a users id in their JWT it’s not really of any use to them.

We then have some exported functions. The login function handles sending the JWT back to the user (which we will eventually store in the Ionic 2 application) so that they can use it to authenticate each request they make to our server. As you will see when we define the routes, the actual login logic is handled by Passport, so when we hit this login function we know the user is already authenticated.

The register function just takes in a request and creates a new user as long as the data is valid and if a user with the same email address doesn’t already exist. Notice that we set the unhashed password on the user object, and then save it to the database. This is where that function we created using the pre hook before comes in handy, when we call save that function will be invoked and it will hash the password before the object is actually saved to the database. Once the user is created, we also send back a JWT to the user just like in the login function.

Finally, we have the coolest function which is roleAuthorization. This function allows us to define an array of roles on each route, and it will only allow access to the route if the user has the appropriate role stored on their JWT. The function first finds the appropriate user using their _id and then checks if their role is in the array of roles supplied to this function.

7. Set up the Routes

This is the final piece of the puzzle and the thing that is going to tie everything together. Our routes set up the URL endpoints that we can send requests to, and what should happen when those routes are hit.

Modify app/routes.js to reflect the following:

var AuthenticationController = require('./controllers/authentication'),
  TodoController = require('./controllers/todos'),
  express = require('express'),
  passportService = require('../config/passport'),
  passport = require('passport');

var requireAuth = passport.authenticate('jwt', { session: false }),
  requireLogin = passport.authenticate('local', { session: false });

module.exports = function (app) {
  var apiRoutes = express.Router(),
    authRoutes = express.Router(),
    todoRoutes = express.Router();

  // Auth Routes
  apiRoutes.use('/auth', authRoutes);

  authRoutes.post('/register', AuthenticationController.register);
  authRoutes.post('/login', requireLogin, AuthenticationController.login);

  authRoutes.get('/protected', requireAuth, function (req, res) {
    res.send({ content: 'Success' });
  });

  // Todo Routes
  apiRoutes.use('/todos', todoRoutes);

  todoRoutes.get(
    '/',
    requireAuth,
    AuthenticationController.roleAuthorization(['reader', 'creator', 'editor']),
    TodoController.getTodos
  );
  todoRoutes.post(
    '/',
    requireAuth,
    AuthenticationController.roleAuthorization(['creator', 'editor']),
    TodoController.createTodo
  );
  todoRoutes.delete(
    '/:todo_id',
    requireAuth,
    AuthenticationController.roleAuthorization(['editor']),
    TodoController.deleteTodo
  );

  // Set up routes
  app.use('/api', apiRoutes);
};

We first import everything we need, including our controllers and the authentication strategies from Passport. Then we define two different sets of routes, one set for authentication and the other for our todos. Let’s take a look at how some of these will work:

authRoutes.post('/register', AuthenticationController.register);

This first route, which will be accessed through /api/auth/register, handles registering a new user. The route simply invokes the register function in our AuthenticationController and the request data will be sent there to be handled.

authRoutes.post('/login', requireLogin, AuthenticationController.login);

This route, which will be /api/auth/login, handles the login for a user. The difference between this route and the last is that it goes through requireLogin first, which will use the local login strategy from passport, before being passed on to the login function in AuthenticationController. This means the user will have to provide the correct login details before they will be given their JWT.

The next set of routes use the todos endpoint, and also make use of the roleAuthorization function, so let’s talk through those as well.

todoRoutes.get(
  '/',
  requireAuth,
  AuthenticationController.roleAuthorization(['reader', 'creator', 'editor']),
  TodoController.getTodos
);

This route, which is api/todos/, will return all of the todos by calling the getTodos function in the TodoController. Before we get there, though, it is first run through requireAuth. This is just like requireLogin but this time, it uses the JWT strategy from Passport, meaning that the user will already have to be logged in and have a JWT. If they pass that, control is then passed on to the roleAuthorization function which will check if they have the appropriate role (reader, creator, or editor). If that also passes, then the todos will be returned.

todoRoutes.post('/', requireAuth, AuthenticationController.roleAuthorization(['creator','editor']), TodoController.createTodo);

This route is also /api/todos/, but the difference is that this is a post route, not a get route. If a POST request is made, this route will be triggered, but if a GET request is made, the previous route will be triggered. This route requires either the creator or editor roles and will trigger the createTodo function if passed.

todoRoutes.delete('/:todo_id', requireAuth, AuthenticationController.roleAuthorization(['editor']), TodoController.deleteTodo);

This route is more of the same, except that it accepts a DELETE request, and the id of the todo is supplied through the URL. A request to this route might look like /api/todos/32, but it requires an authorised user with the editor role to execute.

Also, notice that we have the following route:

authRoutes.get('/protected', requireAuth, function (req, res) {
  res.send({ content: 'Success' });
});

which doesn’t really do anything, but it is restricted by using requireAuth. What this will allow us to do is check if a user is authenticated (using their JWT) by hitting this URL. We will use this eventually to create “Remember Me” type functionality, where the user will be logged in automatically if they have a valid JWT.

Finally, we add app.use to set up all of the routes we created on the /api endpoint.

Deploy to Heroku

All that’s left now is to push it up to Heroku. I’ve already covered how to do that in this tutorial so if you’re not sure how to do that, make sure to give it a read. Alternatively, you can also run the server locally by using node server.js.

Summary

We now have a fully functional server set up that can authenticate users, assign them a role, and restrict what they can do based on that role. There is nothing in this tutorial that is specific to Ionic 2, we have just created a generic API that could be integrated into just about anything. In the next tutorial we will go through how to create an Ionic 2 application that integrates with this server.

UPDATE: Part 2 is now available

Learn to build modern Angular apps with my course