Token-Based Authentication

  1. Overview
    1. Authentication involves "proving" to a service provider your identity
    2. Most common authentication technique: username and password
    3. Two primary authentication schemes
      1. Cookie-based authentication: Traditional web app authentication that creates a session cookie that the browser must send back to the server in all requests
      2. Token-based authentication: Newer method where client obtains a signed token that gets sent to the server in all requests
    4. Diagram explaining the difference
      cookie-based vs. modern token-based auth
      Image source
    5. Benefits of token-based approach
      1. Cross-domain / CORS - Cookies don't get sent across domains, but tokens can be sent via Ajax to any domain
      2. Stateless - No need for the server to maintain session state
      3. Decoupling - Tokens can be generated anywhere
      4. CSRF - Cross-site requests are no longer a danger without the use of cookies
  2. Creating a JWT
    1. JSON Web Token (JWT) is a standard format for tokens
    2. Many programming languages have libraries that produce JWTs
    3. Use Node package jwt-simple to create a JWT
      var jwt = require('jwt-simple')			
      var token = jwt.encode({ username:'bob' }, 'supersecret')
      
      // token is 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImJvYiJ9.Mm0fNOZMBFOrFu99NlnHdz3jkp5IE_BQCNOz4sh1epQ'
      
      
    4. Three parts of JWT (each separated by period)
      1. Header: Base-64 encoded string that says it's a JWT that is signed with the HMAC256 algorithm
        eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 =
        {"typ":"JWT","alg":"HS256"}
        
      2. Payload: Base-64 encoded information being transmitted
        eyJ1c2VybmFtZSI6ImJvYiJ9 =
        {"username":"bob"}
        
      3. Signature: Use HMAC256 algorithm on the header + payload + secretKey
        Mm0fNOZMBFOrFu99NlnHdz3jkp5IE_BQCNOz4sh1epQ 
        
    5. Important to keep secret key a secret on the server or attacker could create valid JWT with a different payload!
  3. Server using Express
    1. Create Express server that sends back requested HTML files
      var express = require('express');
      var jwt = require('jwt-simple');
      var path = require('path');
      var bcrypt = require('bcrypt-nodejs');
      var bodyParser = require('body-parser');
      
      var app = express();
      app.use(bodyParser.json());
      
      // Route requests to files in the local directory
      app.use(express.static(__dirname + '/'));
      
      app.get('/', function(req, res) {
          res.sendFile(path.resolve(__dirname + '/demo.html'));
      });
      
      app.listen(3000, function() {
          console.log('Server listening on', 3000);
      });
      
    2. Add API endpoint to authenticate user 'bob' and send back JWT
      var user = { username: 'bob',
                   password: 'opensesame' };
      
      // Must be kept a secret!			 
      var secretKey = 'supersecretkey';
      
      // Authenticate user
      app.post('/session', function(req, res, next) {
      
      	// Send back 401 if bad username/password
          if (req.body.username !== user.username || req.body.password !== user.password) {
              return res.sendStatus(401);   // Unauthorized
          }
      
          // Send back a JWT that encodes this user's username
          var token = jwt.encode({username: req.body.username}, secretKey);
          res.send(token);
      });						
      				
    3. Create "restricted" endpoint that requires JWT in X-Auth header that encodes 'bob'; send back a secret message
      app.get('/api/restricted', function(req, res) {
      
          // Must have JWT to access (all headers are lower-cased)
          var token = req.headers['x-auth'];
          if (!token) return res.sendStatus(401);   // Unauthorized
          
      	try {
      		// Get back the username that is encoded in the JWT
      		var user = jwt.decode(token, secretKey);
      
      		// Make sure "bob" was in the JWT
      		if (user.username !== 'bob') {
      			return res.sendStatus(401);   // Unauthorized
      		}
      		res.json({ message: "Message only for Bob." });
      	}
      	catch (ex) {
      		// jwt.decode throws exception if problem decoding
      		return res.sendStatus(401);
      	}
      });
      
  4. AngularJS client
    1. Route template login.html for testing authentication
      <div ng-controller="UserCtrl">
          <p>{{ message }}</p>
          <form>
              <input ng-model="user.username" type="text" placeholder="Username"><br>
              <input ng-model="user.password" type="password" placeholder="Password"><br>
              <input type="button" value="Login" ng-click="login()"><br>
              <input type="button" value="Logout" ng-click="logout()">
          </form>
          <p>
              <input type="button" value="Get Restricted Message" ng-click="getRestricted()">
          </p>
      </div>
      
    2. Controller for login in UserController.js
      1. Controller will save JWT on the browser using sessionStorage
        1. Storage area that's available for the duration of the page session
        2. Session lasts as long as the browser is open and survives page reloads and restores
        3. Opening a page in a new tab or window will cause a new session to be initiated
      2. UserCtrl will hard-code username/password for now
        angular.module('myApp')
            .controller('UserCtrl', ['$scope', '$http', '$window',
                function($scope, $http, $window) {
        
        			// Hard-code values expected by API
                    $scope.user = {username: 'bob', password: 'opensesame'};
        			
        			// Message shown to user
                    $scope.message = '';          
                }
            ]);
        
      3. Login sends username/password to server and gets back JWT
        // Login button
        $scope.login = function() {
        	$http.post('/session', $scope.user)
        		.success(function(data, status, headers, config) {
        
        			// Save JWT so user remains logged-in
        			$window.sessionStorage.token = data;
        			
        			// Make all future requests use this header
        			$http.defaults.headers.common['X-Auth'] = data;
        
        			$scope.message = 'You are authenticated!';	
        		})
        		.error(function(data, status, headers, config) {
        			$scope.message = 'Invalid username or password.';
        		});
        };
        
      4. Logout removes token from session storage and the X-Auth header
        // Logout button
        $scope.logout = function() {
        
        	// Erase the token from the browser
        	delete $window.sessionStorage.token;
        
        	// Remove header so user is no longer authenticated
        	delete $http.defaults.headers.common['X-Auth'];
        
        	$scope.message = 'User is logged-out.';
        };
        
      5. Test to see if user can get "secret" message
        // Only works if "bob" has authenticated!
        $scope.getRestricted = function() {
        	$scope.message = '';
        	$http.get('/api/restricted')
        		.success(function(data, status, headers, config) {
        			$scope.message = data.message;
        		})
        		.error(function(data, status, headers, config) {
        			$scope.message = 'Unauthorized';
        		});
        };
        
      6. If page is refreshed, verify user is logged in or not
        // Code must execute when control is loaded
        if ($window.sessionStorage.token) {
        	$scope.message = 'You are authenticated!';
        	$http.defaults.headers.common['X-Auth'] = $window.sessionStorage.token;
        }
        
  5. Password hashes
    1. Storing/comparing raw passwords is a really bad idea
    2. Use bcrypt-nodejs to create hash for passwords
      npm install --save bcrypt-nodejs
      
    3. Generate a hash
      var bcrypt = require('bcrypt-nodejs');
      bcrypt.hash('opensesame', null, null, function(err, hash) {
      
      	// Display hash of 'opensesame'
      	console.log(hash) 
      });
      
    4. Comparing unhashed password with password hash
      // Hash of 'opensesame'
      var passwordHash = '$2a$10$BmC7fiDWqtLKCy3AgiQGGemJVYIeMmIg3/DqR9.phk1r3B3bTutZ.';
      
      // See if 'opensesame' would hash to this
      bcrypt.compare('opensesame', passwordHash, function(err, valid) {
      	if (err) console.log(err);
      	if (valid) 
      		console.log('Same!');
      	else
      		console.log('Not the same');
      });
      
    5. Alter server to use bcrypt
      var bcrypt = require('bcrypt-nodejs');
      
      var user = { username: 'bob',
                   password: '$2a$10$BmC7fiDWqtLKCy3AgiQGGemJVYIeMmIg3/DqR9.phk1r3B3bTutZ.' };
      			
      // Authenticate user
      app.post('/session', function(req, res, next) {
      
          bcrypt.compare(req.body.password, user.password, function(err, valid) {
              if (err) return next(err);
              if (!valid) return res.sendStatus(401);
      		
      		// Send back token to authenticated user
              var token = jwt.encode({username: user.username}, secretKey);
              res.send(token);
          });
      });