MEAN Authentication with JWT (JSON Web Tokens)

Objectives
Compare and contrast a cookie-session and webtoken authentication
Boostrap JWT authentication in a MEAN application

Background

With Express and Ruby we learned the Cookie-Session method of authentication; however, there is a better way to do communicate authentication with Single Page Application and Service-Based Architecture. We're going to use an encrypted chunk of JSON called a JSON Web Token or JWT (pronounced ''jot'') to communicate authentication between client and server.

cookie-token-auth

Reference auth0.com

Why Use JWT?

Aren't you tired of worrying about keeping track of all these things?

  1. Sessions - JWT doesn't require sessions
  2. Cookies - JWT you just save the token to the client
  3. CSRF - Send the JWT instead of a CSRF token
  4. CORS - Forget about it, if your JWT is valid, the data is on its way

Also these benefits:

  1. Speed - you don't have to look up the session
  2. Storage - you don't have to store the session
  3. Mobile Ready - Apps don't let you set cookies!
  4. Testing - you don't have to make loging in a special case in your tests

JWT Flow

Reference: blog.matoski.com

  1. Client logs in
  2. Client receives a token and stores it in localStorage or sessionStorage.
  3. Client does requests with the token using an AngularJS Interceptor
  4. Token gets decoded on the server
  5. Use token data to decide if user has access to the resource, otherwise return a 401 message

JWT FTW

A JWT is pretty easy to identify. It is three strings separated by .

  aaaaaaaaaa.bbbbbbbbbbb.cccccccccccc

Each part has a different significance:

jwt

Here is a JWT Example:

var header = {
  "typ": "JWT",
  "alg": "HS256"
}

Payload

var payload = {
  "iss": "scotch.io",
  "exp": 1300819380,
  "name": "Chris Sevilleja",
  "admin": true
}

Signature

var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);

HMACSHA256(encodedString, 'secret');

REMEMBER The 'secret' acts as an encryption string known only by the two parties communicating via JWT. Protect your secrets!

JSON Web Token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJleHAiOjEzMDA4MTkzODAsIm5hbWUiOiJDaHJpcyBTZXZpbGxlamEiLCJhZG1pbiI6dHJ1ZX0.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773

Further Reading

Features of mean-auth

User Service

The User service needs some custom routes:

app.factory('User', ['$resource', 'HOST', function ($resource, HOST) {
  return $resource(HOST + '/api/users/:id', { id: '@id' }, {
    update: { method: 'PUT' },
    sign_up: { url: HOST + '/api/users', method: 'POST', isArray: false },
    login: { url: HOST + '/api/users/login', method: 'POST', isArray: false },
    logout: { url: HOST + '/api/users/logout', method: 'GET', isArray: false }
  })
}])

Auth Service

So far we've used services like "client-side models" that connect to our RESTful API with $resource, but you can also tuck away functions into services and an Authentication Service is a very nice group of functions.

app.factory('Auth', [function() {
  return {
    isLoggedIn: function () {
      return !!localStorage.getItem('jwtToken')
    }
  }
}])

Angular Interceptors

An Angular interceptors allow you to "intercept" http requests and responses and change them. We use an interceptor to attach the JWT to every outgoing http request, and handle what to do with 401 (Unauthorized) statuses in any http response.

// SERVICES.JS

app.factory('authInterceptor', function ($rootScope, $q, $window) {
  return {
    request: function (config) {
      config.headers = config.headers || {};
      if ($window.localStorage.jwtToken) {
        config.headers.Authorization = 'Bearer ' + $window.localStorage.jwtToken;
      }
      return config;
    },
    response: function (response) {
      if (response.status === 401) {
        // handle the case where the user is not authenticated
      }
      return response || $q.when(response);
    }
  };
})

app.config(function ($httpProvider) {
  $httpProvider.interceptors.push('authInterceptor');
});

$rootScope.$broadcast('taco') & $rootScope.$on('taco', callback)

Use AngularJS's $broadcast and $on to notify other controllers that you logged in:

  $rootScope.$broadcast('loggedIn'); // TELL THE OTHER CONTROLLERS WE'RE LOGGED IN
  $rootScope.$on('loggedIn', function () {
    $scope.isLoggedIn = true
  })

Challenges

Fork this mean-auth sample project and add a sign-up method.

Goal Get the mean-auth app running locally

  1. Clone the mean-auth
    $ git fork https://github.com/ajbraus/mean-auth
    $ cd mean-auth
    $ nodemon
    

Goal Add a sign-up feature

  1. Add '/sign-up' template, route & controller

    // app.js
    
    .when('/sign-up', {
     templateUrl: 'templates/sign-up'
    , controller: 'SignUpCtrl'
    })
    
  2. Add the email to the JWT
  3. Sign the JWT and send it back to the client and save it
  4. After signup go to '/'
  5. Notify the rest of the app that you are logged in with $rootScope.$broadcast('loggedIn')

Goal: Hash passwords

  1. Install and bcrypt
    $ npm install --save bcrypt
    
  2. Add bcrypt and salt to your user.js

     // user.js
    
     bcrypt = require('bcrypt'),
     salt = bcrypt.genSaltSync(10);
    
  3. Add the createSecure, authenticate and checkPassword functions to the User model.

    //
    // user.js
    //
    
    // ...
    
    UserSchema.statics.createSecure = function (email, password, callback) {
     // `this` references our schema
     // store it in variable `that` because `this` changes context in nested callbacks
     var that = this;
    
     // hash password user enters at sign up
     bcrypt.genSalt(function (err, salt) {
       bcrypt.hash(password, salt, function (err, hash) {
         console.log(hash);
    
         // create the new user (save to db) with hashed password
         that.create({
           email: email,
           passwordDigest: hash
         }, callback);
       });
     });
    };
    
    UserSchema.statics.authenticate = function (email, password, callback) {
     this.findOne({email: email}, function (err, user) {
       console.log(user);
       if (user === null) {
         callback('Can\'t find user with email ' + email, user);
       } else if (user.checkPassword(password)) {
         callback(null, user);
       }
     });
    };
    
    UserSchema.methods.checkPassword = function (password) {
     return password == this.password;
    };
    
  4. Use the User.createSecure method to create a user at signup.
  5. Add the authenticate function to the /api/users/login route
     User.authenticate(req.body.email, req.body.password, function(error, user) {
       if (error) {
         res.send(error)
       } else if (user) {
         // CREATE, SIGN, AND SEND TOKEN HERE
       }
     });
    

Extra Credit: Validate that confirm password and password match in client Extra Credit: Submit a pull request back to mean-auth

Or if you want everything easy:

Satellizer