Tutorial hero
Lesson icon

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

Originally published October 13, 2016 Time 32 mins

In this tutorial series, we are walking through how to create a role-based authentication system with Ionic 2 using Passport. In short, we want to create a way to provide different users with different permissions, or roles, in the application. One example of this would be a chat room that allows all users to post messages, but only some users (moderators) are allowed to kick other users out.

The example we are building is a shared todo application, where the users can have one of three different roles:

  • A reader who can only read todos
  • A creator who can read todos, and create new todos
  • An editor who can read, create, and delete todos

In Part 1 of the tutorial we worked on creating the backend server using NodeJS, MongoDB, Express, and Passport, which would support this functionality. We created an API that allows us to:

  • Register a new user
  • Authenticate a user and assign them a JWT (JSON Web Token)
  • Check the validity of a JWT that a user possesses
  • Get a list of all todos
  • Create new todos
  • Delete a specific todo

Each feature will have its own route, a URL endpoint that the application can make a request to, and the important part is that we can restrict access to each route based on the role a user has. To refresh your memory, to restrict the Delete Todo route to only users with the editor role, we could do the following:

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

Up until this point, nothing has really been specific to Ionic 2. You could use this server with just about any front end technology. However, in this tutorial, I will be walking you through exactly how to make use of this API in an Ionic 2 application. It’s not all that different to creating a more basic todo application, except that we will be making requests to a server instead of storing data locally (and there’s a little bit of added complexity with the JWTs, but we’ll get to 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.

This tutorial will be building on top of the last tutorial, so you should make sure you complete that before starting this tutorial:

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. Generate a New Ionic 2 Application

In the last tutorial, you would have created a folder structure that included both a server and a client folder. All of the work we did in the last tutorial was in the server folder, but in this tutorial, we will be creating our Ionic 2 application in the client folder.

Make the client folder your current working directory:

cd client

Run the following command to generate a new Ionic 2 application:

ionic start todo-roles blank --v2

Once that has finished generating, we are also going to set up a few pages and providers. We already have a home page that is automatically generated that we will use for displaying todos, so we will just need to create a login page and a signup page.

Run the following commands to generate pages for the application:

ionic g page LoginPage
ionic g page SignupPage

We are also going to create two providers for the application. One will communicate with our API to handle authentication, and the other will communicate with our API to handle todos.

Run the following commands to generate providers for the application:

ionic g provider Auth
ionic g provider Todos

We will need to set these pages and providers up in our app.module.ts file in order to use them throughout the application, so let’s do that now.

Modify src/app/app.module.ts to reflect the following:

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { Storage } from '@ionic/storage';
import { HomePage } from '../pages/home/home';
import { LoginPage } from '../pages/login-page/login-page';
import { SignupPage } from '../pages/signup-page/signup-page';
import { Todos } from '../providers/todos';
import { Auth } from '../providers/auth';

@NgModule({
  declarations: [MyApp, HomePage, LoginPage, SignupPage],
  imports: [IonicModule.forRoot(MyApp)],
  bootstrap: [IonicApp],
  entryComponents: [MyApp, HomePage, LoginPage, SignupPage],
  providers: [Storage, Todos, Auth],
})
export class AppModule {}

We’ve set up our pages and our providers now, and we’ve also added in Ionic’s Storage service, which we will use to store the users JWT locally. Now we’re ready to start building!

2. Set up the Auth Provider

We are first going to implement the login and signup system, using the auth routes we created in the API. We will eventually need to create an interface in our application to allow the user to interact with the app, but an important step before we get to this is setting up the Auth provider we generated earlier.

The Auth provider will handle communicating with our backend, and will make it easy for us to perform the following tasks:

  • Checking the validity of a user’s JWT
  • Creating a new account for a user
  • Allowing a user to log in
  • Allowing a user to log out

Let’s create the provider first, and then we will walk through it.

Modify src/providers/auth.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { Storage } from '@ionic/storage';
import 'rxjs/add/operator/map';

@Injectable()
export class Auth {

  public token: any;

  constructor(public http: Http, public storage: Storage) {

  }

  checkAuthentication(){

    return new Promise((resolve, reject) => {

        //Load token if exists
        this.storage.get('token').then((value) => {

            this.token = value;

            let headers = new Headers();
            headers.append('Authorization', this.token);

            this.http.get('https://YOUR_HEROKU_APP.herokuapp.com/api/auth/protected', {headers: headers})
                .subscribe(res => {
                    resolve(res);
                }, (err) => {
                    reject(err);
                });

        });

    });

  }

  createAccount(details){

    return new Promise((resolve, reject) => {

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        this.http.post('https://YOUR_HEROKU_APP.herokuapp.com/api/auth/register', JSON.stringify(details), {headers: headers})
          .subscribe(res => {

            let data = res.json();
            this.token = data.token;
            this.storage.set('token', data.token);
            resolve(data);

          }, (err) => {
            reject(err);
          });

    });

  }

  login(credentials){

    return new Promise((resolve, reject) => {

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        this.http.post('https://YOUR_HEROKU_APP.herokuapp.com/api/auth/login', JSON.stringify(credentials), {headers: headers})
          .subscribe(res => {

            let data = res.json();
            this.token = data.token;
            this.storage.set('token', data.token);
            resolve(data);

            resolve(res.json());
          }, (err) => {
            reject(err);
          });

    });

  }

  logout(){
    this.storage.set('token', '');
  }

}

We are importing Http to make HTTP requests, and Storage to store and retrieve information, which are pretty common. But, we are also importing Headers from @angular/http which you often don’t need. Being able to set headers is important for us here as it allows us to specify the content type of the data we are sending to the backend, and it also allows us to attach the users JWT as an Authorization header. If our backend receives a request to a protected route, and the request does not contain an Authorization header with a valid JWT, the request will be rejected.

In the checkAuthentication function, we grab the users JWT from storage. This token may or may not exist yet. We then check the validity of the JWT by making a request to the auth/protected route with the retrieved JWT. This route doesn’t actually do anything, but we have set it up as a route that requires an authorised user, so if the request fails we know that the user is not logged in. This allows us to easily check whether a user is logged in or not.

The createAccount function simply takes in the details for a new user (which will contain an email and a password), and sends those details in a POST request to the auth/register route. If this request is successfuly, the server will respond with a new JWT for the user, which we then store in Storage.

The login function is similar to the createAccount function, except that the user provides details for an existing account and the request is sent to auth/login. If the credentials are correct, the server will also respond with a new JWT for the user, which we again store with Storage to be used later.

Finally, the logout function simply clears the JWT that the user has stored, this means that any future requests they make to the server will be unauthorised.

3. Set up the Authentication Interface

Now that we have created all the functions we require in our Auth provider, we can create the login page and the signup page. We will use these pages to send the appropriate data to the Auth provider.

By default, the home component will be used for the first page in the application. We are planning on using that page to display the list of todos, though, so we do not want to show it first. Instead, we want to display the login page first.

Modify src/app/app.component.ts to reflect the following:

import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar } from 'ionic-native';

import { LoginPage } from '../pages/login-page/login-page';

@Component({
  template: `<ion-nav [root]="rootPage"></ion-nav>`,
})
export class MyApp {
  rootPage = LoginPage;

  constructor(platform: Platform) {
    platform.ready().then(() => {
      StatusBar.styleDefault();
    });
  }
}

Now the login page will be displayed when the user first opens the app. Let’s create that now.

3.1 Set up the Login Page

In order to set up the login page we will need to create both the template file and the class definition. We will also add some styling in the .scss file as well.

Modify src/pages/login-page/login-page.html to reflect the following:

<ion-header>
  <ion-navbar color="secondary">
    <ion-title>Shared Todos</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-row class="login-form">
    <ion-col>
      <ion-list inset>
        <ion-item>
          <ion-label><ion-icon name="person"></ion-icon></ion-label>
          <ion-input
            [(ngModel)]="email"
            placeholder="email"
            type="text"
          ></ion-input>
        </ion-item>

        <ion-item>
          <ion-label><ion-icon name="lock"></ion-icon></ion-label>
          <ion-input
            [(ngModel)]="password"
            placeholder="password"
            type="password"
          ></ion-input>
        </ion-item>
      </ion-list>

      <button
        ion-button
        full
        (click)="login()"
        color="primary"
        class="login-button"
      >
        Login
      </button>
    </ion-col>
  </ion-row>

  <ion-row>
    <ion-col>
      <button ion-button (click)="launchSignup()" class="create-account">
        Create an Account
      </button>
    </ion-col>
  </ion-row>
</ion-content>

There’s nothing too fancy going on here, we just create a simple form with two-way data binding that will accept an email and a password. In a moment, we will make use of those values to actually log the user in. As well as a form allowing the user to input their credentials, we also have a button that will launch the Create Account page.

Modify src/pages/login-page/login-page.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController, LoadingController } from 'ionic-angular';
import { Auth } from '../../providers/auth';
import { HomePage } from '../home/home';
import { SignupPage } from '../signup-page/signup-page';

@Component({
  selector: 'login-page',
  templateUrl: 'login-page.html'
})
export class LoginPage {

    email: string;
    password: string;
    loading: any;

    constructor(public navCtrl: NavController, public authService: Auth, public loadingCtrl: LoadingController) {

    }

    ionViewDidLoad() {

        this.showLoader();

        //Check if already authenticated
        this.authService.checkAuthentication().then((res) => {
            console.log("Already authorized");
            this.loading.dismiss();
            this.navCtrl.setRoot(HomePage);
        }, (err) => {
            console.log("Not already authorized");
            this.loading.dismiss();
        });

    }

    login(){

        this.showLoader();

        let credentials = {
            email: this.email,
            password: this.password
        };

        this.authService.login(credentials).then((result) => {
            this.loading.dismiss();
            console.log(result);
            this.navCtrl.setRoot(HomePage);
        }, (err) => {
            this.loading.dismiss();
            console.log(err);
        });

    }

    launchSignup(){
        this.navCtrl.push(SignupPage);
    }

    showLoader(){

        this.loading = this.loadingCtrl.create({
            content: 'Authenticating...'
        });

        this.loading.present();

    }

}

There’s quite a bit more going on in this file, but it’s pretty straight forward once you break it down. One important part of this class is the use of ionViewDidLoad, which will trigger as soon as the page is loaded, to automatically log the user in. As soon as the page is ready, we check if the user already has a valid JWT (meaning that they should be logged in) by calling the checkAuthentication function in the Auth provider. If they do already have a valid JWT then we automatically go to the home page, otherwise, we stay on the login page. This essentially adds “Remember Me” type functionality to your application.

The login function simply handles passing on the data entered by the user onto the login function in the Auth provider. If the login is successful, then we send the user to the home page, otherwise, we stay on the login page.

We’ve also created a launchSignup function to handle navigation to the create account (or signup) page, and a showLoader function that handles creating loading overlay for us whilst requests are being made to the user. Requests to the server can take a few seconds, so it’s important to show the user that something is happening during this time, otherwise they may think the app is just frozen.

We’re also going to add a bit of styling to the login page, this isn’t really important to the tutorial but it will make it look a bit nicer.

Modify src/pages/login-page/login-page.scss to reflect the following:

.ios,
.md {
  login-page {
    ion-content {
      background-color: map-get($colors, secondary);
    }

    scroll-content {
      display: flex;
      flex-direction: column;
    }

    ion-row {
      align-items: center;
      text-align: center;
    }

    ion-item {
      border-radius: 30px !important;
      padding-left: 10px !important;
      margin-bottom: 10px;
      background-color: #f6f6f6;
      opacity: 0.7;
      font-size: 0.9em;
    }

    ion-list {
      margin: 0;
    }

    .login-logo {
      flex: 2;
    }

    .login-form {
      flex: 1;
    }

    .create-account {
      color: #fff;
      text-decoration: underline;
      background: none;
    }

    .login-button {
      border-radius: 30px;
      font-size: 0.9em;
      background-color: transparent;
      border: 1px solid #fff;
    }
  }
}

3.2 Set up the Signup Page

Now we just need to create the signup page, which will allow a user to create an account if they don’t have one already. Since this whole authentication system is based around roles, we need a way to give different roles to different users. We will just be adding a select input to the signup page that will allow a user to select their own role. Of course, in a real-world scenario we most likely wouldn’t allow the user to choose their own role, but it makes it easier for the sake of the tutorial and testing.

Modify src/pages/signup-page/signup-page.html to reflect the following:

<ion-header>
  <ion-navbar color="secondary">
    <ion-title>Create Account</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-row class="account-form">
    <ion-col>
      <ion-list inset>
        <ion-item>
          <ion-label><ion-icon name="mail"></ion-icon></ion-label>
          <ion-input
            [(ngModel)]="email"
            placeholder="Email"
            type="email"
          ></ion-input>
        </ion-item>

        <ion-item>
          <ion-label><ion-icon name="lock"></ion-icon></ion-label>
          <ion-input
            [(ngModel)]="password"
            placeholder="Password"
            type="password"
          ></ion-input>
        </ion-item>

        <ion-item>
          <ion-label>Role</ion-label>
          <ion-select [(ngModel)]="role">
            <ion-option value="reader">Reader</ion-option>
            <ion-option value="creator">Creator</ion-option>
            <ion-option value="editor">Editor</ion-option>
          </ion-select>
        </ion-item>
      </ion-list>

      <button ion-button (click)="register()" class="continue-button">
        Register
      </button>
    </ion-col>
  </ion-row>
</ion-content>

Again, this is a pretty simple template that accepts an email and a password, but we also use an <ion-select> to allow the user to choose a specific role.

Modify src/pages/signup-page/signup-page.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController, LoadingController } from 'ionic-angular';
import { Auth } from '../../providers/auth';
import { HomePage } from '../home/home';

@Component({
  selector: 'signup-page',
  templateUrl: 'signup-page.html'
})
export class SignupPage {

  role: string;
  email: string;
  password: string;

  constructor(public navCtrl: NavController, public authService: Auth, public loadingCtrl: LoadingController) {

  }

  register(){

    this.showLoader();

    let details = {
        email: this.email,
        password: this.password,
        role: this.role
    };

    this.authService.createAccount(details).then((result) => {
      this.loading.dismiss();
      console.log(result);
      this.navCtrl.setRoot(HomePage);
    }, (err) => {
        this.loading.dismiss();
    });

  }

  showLoader(){

    this.loading = this.loadingCtrl.create({
      content: 'Authenticating...'
    });

    this.loading.present();

  }

}

This class is very similar to the login page, the only thing different really is that we are calling the createAccount function in the Auth provider, instead of the login function.

We are also going to add some styling to the signup page as well.

Modify src/pages/signup-page/signup-page.scss to reflect the following:

.ios,
.md {
  signup-page {
    ion-content {
      background-color: map-get($colors, secondary);
    }

    scroll-content {
      display: flex;
      flex-direction: column;
    }

    ion-item {
      border-radius: 30px !important;
      padding-left: 10px !important;
      margin-bottom: 10px;
      background-color: #f6f6f6;
      opacity: 0.7;
      font-size: 0.9em;
    }

    ion-list {
      margin: 0;
    }

    .heading-text {
      flex: 1;
      align-items: center;
      text-align: center;
    }

    .heading-text h3 {
      color: #fff;
    }

    .account-form {
      flex: 1;
    }

    .continue-button {
      width: 100%;
      border-radius: 30px;
      margin-top: 30px;
      font-size: 0.9em;
      background-color: transparent;
      border: 1px solid #fff;
    }
  }
}

Now you should have a fully functioning authentication system with a login page and create account functionality. Right now, the user will just be redirected to an empty home page upon a successful authentication. In the next part, we will handle all of the todo related functionality.

4. Setting up the Todo Provider

We now have the ability to create new users, assign them a role, and authenticate them. Now we need to provide users with a way to view the list of todos, as well as allowing them to create their own todos and delete them if they have permission to do that.

In order to do this, we are going to create a Todos provider. This will be similar to the Auth provider that we created earlier, except that it will deal with the todo routes instead.

Modify src/providers/todos.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { Auth } from './auth';
import 'rxjs/add/operator/map';

@Injectable()
export class Todos {

  constructor(public http: Http, public authService: Auth) {

  }

  getTodos(){

    return new Promise((resolve, reject) => {

      let headers = new Headers();
      headers.append('Authorization', this.authService.token);

      this.http.get('https://YOUR_HEROKU_APP.herokuapp.com/api/todos', {headers: headers})
        .map(res => res.json())
        .subscribe(data => {
          resolve(data);
        }, (err) => {
          reject(err);
        });
    });

  }

  createTodo(todo){

    return new Promise((resolve, reject) => {

      let headers = new Headers();
      headers.append('Content-Type', 'application/json');
      headers.append('Authorization', this.authService.token);

      this.http.post('https://YOUR_HEROKU_APP.herokuapp.com/api/todos', JSON.stringify(todo), {headers: headers})
        .map(res => res.json())
        .subscribe(res => {
          resolve(res);
        }, (err) => {
          reject(err);
        });

    });

  }

  deleteTodo(id){

    return new Promise((resolve, reject) => {

        let headers = new Headers();
        headers.append('Authorization', this.authService.token);

        this.http.delete('https://YOUR_HEROKU_APP.herokuapp.com/api/todos/' + id, {headers: headers}).subscribe((res) => {
            resolve(res);
        }, (err) => {
            reject(err);
        });

    });

  }

}

As I mentioned, this is very similar to the Auth provider, and we also make use of Headers here to authenticate each request to the server with the users JWT.

The getTodos function will return a list of all of the todos that have been added, but only for authenticated users.

The createTodo function will allow users to create a new todo, but the request to the server will be rejected if the user does not have the appropriate role (which is defined in their JWT).

The deleteTodo function will handle making a request to delete a specific todo. Again, if the user does not have the appropriate role then the request will be rejected. This function is slightly different, because we also append the id of the todo to the end of the route.

5. Set up the Todo Interface

Now that we have the provider set up, we can create the interface to interact with it.

Modify src/pages/home/home.html to reflect the following:

<ion-header>
  <ion-navbar color="secondary">
    <ion-title> Todo Roles </ion-title>
    <ion-buttons end>
      <button ion-button icon-only (click)="addTodo()">
        <ion-icon name="add"></ion-icon>
      </button>
    </ion-buttons>
    <ion-buttons start>
      <button ion-button icon-only (click)="logout()">
        <ion-icon name="power"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-list no-lines>
    <ion-item-sliding *ngFor="let todo of todos">
      <ion-item> {{todo.title}} </ion-item>

      <ion-item-options>
        <button ion-button color="danger" (click)="deleteTodo(todo)">
          <ion-icon name="trash"></ion-icon>
          Delete
        </button>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
</ion-content>

In this template we have created a list that will loop over the todos array that we will add to our class in the next step. We create a sliding item for each todo, so that users are provided with an option to delete the todo. We also add two buttons to the <ion-navbar>, one that will allow the user to create a new todo, and one that will log the user out.

Modify src/pages/home/home.ts to reflect the following:

import { Component } from "@angular/core";
import { NavController, ModalController, AlertController, LoadingController } from 'ionic-angular';
import { Todos } from '../../providers/todos';
import { Auth } from '../../providers/auth';
import { LoginPage } from '../login-page/login-page';

@Component({
  selector: 'home-page',
  templateUrl: 'home.html'
})
export class HomePage {

  todos: any;
  loading: any;

  constructor(public navCtrl: NavController, public todoService: Todos, public modalCtrl: ModalController,
    public alertCtrl: AlertController, public authService: Auth, public loadingCtrl: LoadingController) {

  }

  ionViewDidLoad(){

    this.todoService.getTodos().then((data) => {
          this.todos = data;
    }, (err) => {
        console.log("not allowed");
    });

  }

  addTodo(){

    let prompt = this.alertCtrl.create({
      title: 'Add Todo',
      message: 'Describe your todo below:',
      inputs: [
        {
          name: 'title'
        }
      ],
      buttons: [
        {
          text: 'Cancel'
        },
        {
          text: 'Save',
          handler: todo => {

                if(todo){

                    this.showLoader();

                    this.todoService.createTodo(todo).then((result) => {
                        this.loading.dismiss();
                        this.todos = result;
                        console.log("todo created");
                    }, (err) => {
                        this.loading.dismiss();
                        console.log("not allowed");
                    });

                }


          }
        }
      ]
    });

    prompt.present();

  }

  deleteTodo(todo){

    this.showLoader();

    //Remove from database
    this.todoService.deleteTodo(todo._id).then((result) => {

      this.loading.dismiss();

      //Remove locally
        let index = this.todos.indexOf(todo);

        if(index > -1){
            this.todos.splice(index, 1);
        }

    }, (err) => {
      this.loading.dismiss();
        console.log("not allowed");
    });
  }

  showLoader(){

    this.loading = this.loadingCtrl.create({
      content: 'Authenticating...'
    });

    this.loading.present();

  }

  logout(){

    this.authService.logout();
    this.navCtrl.setRoot(LoginPage);

  }

}

There’s quite a bit going on in this file, so let’s walk through it. First, we are making use of ionViewDidLoad again, to immediately make a request to load the todos from the server once the page is loaded. Once the todos are retrieved, they are added to the member variable todos, which is what we loop over in the template.

Instead of creating a new page for adding todos, the addTodo function simply launches a prompt using the AlertController to get the title of the todo from the user. The important thing about this function, is that once we add the todo, the server will also send us back a list of all the todos again, which we once more set on the todos member variable. It’s important that we do this, rather than just pushing the new todo locally to the array we already have, because when we create a new todo it is automatically assigned an id by our server. We don’t know what this id is until the server tells us, so if we didn’t “reset” the list in this way, if we tried to delete the todo we just added it wouldn’t work, because the id would be wrong.

The deleteTodo function simply passes on the id we were just talking about to the deleteTodo function in the Todos provider. Once we get a successfuly response we remove the todo locally as well. If we don’t get a successful response, then we don’t remove the todo locally because it means the user is not authorised to delete todos.

We also have a showLoader function to handle displaying the loading overlay, and a logout function to log the user out and send them back to the login page.

Once more, we are going to add just a little bit of styling to this page.

Modify src/pages/home/home.scss to reflect the following:

.ios,
.md {
  home-page {
    ion-content {
      background-color: map-get($colors, secondary);
    }
  }
}

Summary

This was a very large tutorial that covered a lot of complex concepts, but we are finished! You should now have a fully functional app that will allow you to:

  • Create and authenticate users
  • Assign roles to specific users
  • Control what functionality can be accessed based on the user’s role
  • Use JWTs to securely authenticate every request, and to allow the user to remain logged in

Although we’ve implemented a specific example in this tutorial, this same approach could be used to create just about any application that requires some kind of role based access. Even without the use of roles, it is still a well structured authentication system in general.

Learn to build modern Angular apps with my course