April 05 2016

AngularJS JWT Authentication Example & Tutorial

Following up on a post I did a while back on how to implement Basic HTTP Authentication in AngularJS, I thought it was time to do an updated example/tutorial showing how to do the same thing (setup a login page) with JWT in AngularJS.

UPDATE 16 Aug 2016 - For the same example written in Angular 2 check out Angular 2 JWT Authentication Example & Tutorial.

The project is available on GitHub at https://github.com/cornflourblue/angular-jwt-authentication-example.

On the angular side of things there's actually very little difference between basic HTTP auth and JWT auth, the angular app just sends different data in the HTTP Authorization header for API calls. With basic auth the angular app sends the base64 encoded username and password prefixed with 'Basic ', and with JWT the app sends a base64 encoded JSON Web Token (JWT) prefixed with 'Bearer '. For more info on JSON Web Tokens check out jwt.io.

Here it is in action: (See on Plunker at http://plnkr.co/edit/RqUpIn?p=preview)

The example uses a fake backend with the help of the angular $httpBackend service which is part of the ngMockE2E module, this is done so the example will work without a real backend api (backend-less) in browser only environments like plunker. To use a real backend just remove the fake-backed.js and angular-mocks.js scripts from the main index.html file, and remove the 'ngMockE2E' dependency from the main app.js file.

For a full stack example that uses the MEAN stack (NodeJS on the backend) and includes user registration you can check out MEAN Stack User Registration and Login Example & Tutorial, it also uses JWT but is structured a little differently in that it uses a separate standalone login page rather than having it built into the angular app.

AngularJS Project Structure

I've based a lot of the project and code structure on recommendations from John Papa's angular 1 style guide with my own tweaks here and there, for example I've placed application config and routes within the app.js file rather than separate files because there isn't much code in either section, these could be split out later if the sections grow.

I also like placing application code such as services, helpers, directives, content etc in folders prefixed with 'app-' to prevent having a name clash with a feature folder, for example if I need to add a section to my app called 'services' or 'content', it also has the added benefit of grouping all the 'non-gui' code together at the top of the folders.

Here's what the project structure looks like, click on any of the files or folders for the description and code:

 

AngularJS App Content Folder

Path: /app-content

The app content folder is used for static content such as css, images, fonts etc.

 

AngularJS App CSS

Path: /app-content/app.css

The app css file is a stylesheet containing any custom styles for the angular application, the only custom style I've added for the example sets the font-size of the validation messages on the login form to slightly smaller than the bootstrap default.

.help-block {
    font-size: 12px;
}
 

AngularJS App Helpers Folder

Path: /app-helpers

The app helpers folder is for bits and pieces that don't fit anywhere else but aren't big enough to justify they're own folder, such as the fake backend.

 

AngularJS Fake Backend

Path: /app-helpers/fake-backend.js

The fake backend enables the example to run without a real backend api (a.k.a. 'backend-less'). It uses the $httpBackend service provided by the ngMockE2E module to intercept http requests made by the angular app and send back mock/fake responses.

It contains a mock implementation of the '/api/authenticate' api end point which validates the username and password and sends back a fake jwt token if successful. All other requests are passed through to the server so static files (.js, .html, .css etc) required by the angular app are served correctly.

(function () {
    'use strict';

    angular
        .module('app')
        .run(setupFakeBackend);

    // setup fake backend for backend-less development
    function setupFakeBackend($httpBackend) {
        var testUser = { username: 'test', password: 'test', firstName: 'Test', lastName: 'User' };

        // fake authenticate api end point
        $httpBackend.whenPOST('/api/authenticate').respond(function (method, url, data) {
            // get parameters from post request
            var params = angular.fromJson(data);

            // check user credentials and return fake jwt token if valid
            if (params.username === testUser.username && params.password === testUser.password) {
                return [200, { token: 'fake-jwt-token' }, {}];
            } else {
                return [200, {}, {}];
            }
        });

        // pass through any urls not handled above so static files are served correctly
        $httpBackend.whenGET(/^\w+.*/).passThrough();
    }
})();
 

AngularJS App Services Folder

Path: /app-services

The services folder is used for custom AngularJS services / factories. All api access and business logic should be placed in services within this folder in order to keep angular controllers thin and to maintain clean separation of concerns.

 

AngularJS Authentication Service

Path: /app-services/authentication.service.js

The authentication service is used for logging in and out of the application, to login it posts the users credentials to the api and checks if there's a jwt token in the response, if so the login is successful so the user details are stored in local storage and the token is added to the http authorization header for all requests made by the $http service.

The current logged in user details are stored in local storage so the user will stay logged in if they refresh the browser and between browser sessions until they logout. If you don't want the user to stay logged in between refreshes/sessions, this behaviour could easily be changed by using something other than local storage to hold the current user (e.g. session storage or root scope).

(function () {
    'use strict';

    angular
        .module('app')
        .factory('AuthenticationService', Service);

    function Service($http, $localStorage) {
        var service = {};

        service.Login = Login;
        service.Logout = Logout;

        return service;

        function Login(username, password, callback) {
            $http.post('/api/authenticate', { username: username, password: password })
                .success(function (response) {
                    // login successful if there's a token in the response
                    if (response.token) {
                        // store username and token in local storage to keep user logged in between page refreshes
                        $localStorage.currentUser = { username: username, token: response.token };

                        // add jwt token to auth header for all requests made by the $http service
                        $http.defaults.headers.common.Authorization = 'Bearer ' + response.token;

                        // execute callback with true to indicate successful login
                        callback(true);
                    } else {
                        // execute callback with false to indicate failed login
                        callback(false);
                    }
                });
        }

        function Logout() {
            // remove user from local storage and clear http auth header
            delete $localStorage.currentUser;
            $http.defaults.headers.common.Authorization = '';
        }
    }
})();
 

AngularJS Home Folder

Path: /app-content/app.css

The home folder contains all controllers and views for the home section of the angular app.

 

AngularJS Home Index Controller

Path: /home/index.controller.js

The home index controller handles all interaction and data for the home index view, which at the moment doesn't do anything so it just contains the basic structure that I start all of my controllers with.

(function () {
    'use strict';

    angular
        .module('app')
        .controller('Home.IndexController', Controller);

    function Controller() {
        var vm = this;

        initController();

        function initController() {
        }
    }
})();
 

AngularJS Home Index View

Path: /home/index.view.html

The default view for the home section.

<div class="col-md-6 col-md-offset-3">
    <h1>Home</h1>
    <p>You're logged in with JWT!!</p>
    <p><a href="#/login">Logout</a></p>
</div>
 

AngularJS Login Folder

Path: /login

The login folder contains all controllers and views for the login section of the angular app.

 

AngularJS Login Index Controller

Path: /login/index.controller.js

The login index controller handles all interaction and data for the login index view, when it first loads it ensures the user is logged out (in the initController() function), which is why the logout link on the home index view just needs to send the user to the login page.

The login function exposed by the viewmodel (vm.login) uses the AuthenticationService to validate the users credentials and redirect to the home page (on success) or display and error message (on failure).

(function () {
    'use strict';

    angular
        .module('app')
        .controller('Login.IndexController', Controller);

    function Controller($location, AuthenticationService) {
        var vm = this;

        vm.login = login;

        initController();

        function initController() {
            // reset login status
            AuthenticationService.Logout();
        };

        function login() {
            vm.loading = true;
            AuthenticationService.Login(vm.username, vm.password, function (result) {
                if (result === true) {
                    $location.path('/');
                } else {
                    vm.error = 'Username or password is incorrect';
                    vm.loading = false;
                }
            });
        };
    }
})();
 

AngularJS Login Index View

Path: /login/index.view.html

The login view contains a pretty standard login form with username and password fields, it uses the ngMessages directive for displaying validation messages. I've disabled the built in browser validation with the 'novalidate' attribute on the form so the angular validation messages are displayed on submit if the form is invalid.

<div class="col-md-6 col-md-offset-3">
    <div class="alert alert-info">
        Username: test<br />
        Password: test
    </div>
    <h2>Login</h2>
    <form name="form" ng-submit="form.$valid && vm.login()" novalidate>
        <div class="form-group" ng-class="{ 'has-error': form.$submitted && form.username.$invalid }">
            <label for="username">Username</label>
            <input type="text" name="username" class="form-control" ng-model="vm.username" required />
            <div ng-messages="form.$submitted && form.username.$error" class="help-block">
                <div ng-message="required">Username is required</div>
            </div>
        </div>
        <div class="form-group" ng-class="{ 'has-error': form.$submitted && form.password.$invalid }">
            <label for="password">Password</label>
            <input type="password" name="password" class="form-control" ng-model="vm.password" required />
            <div ng-messages="form.$submitted && form.password.$error" class="help-block">
                <div ng-message="required">Password is required</div>
            </div>
        </div>
        <div class="form-group">
            <button ng-disabled="vm.loading" class="btn btn-primary">Login</button>
            <img ng-if="vm.loading" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
        </div>
        <div ng-if="vm.error" class="alert alert-danger">{{vm.error}}</div>
    </form>
</div>
 

AngularJS Main App File

Path: /app.js

The app.js file is the entry point for the angular application where the app module is declared along with dependencies, and contains configuration and startup logic for when the app is first loaded.

The config() function is used to define the routes of the application using the Angular UI Router, the run() function contains app startup logic including code that checks local storage to keep the user logged in between page refreshes and browser sessions, and adds an event handler to the '$locationChangeStart' event that redirects unauthenticated users to the login page.

(function () {
    'use strict';

    angular
        .module('app', ['ui.router', 'ngMessages', 'ngStorage', 'ngMockE2E'])
        .config(config)
        .run(run);

    function config($stateProvider, $urlRouterProvider) {
        // default route
        $urlRouterProvider.otherwise("/");

        // app routes
        $stateProvider
            .state('home', {
                url: '/',
                templateUrl: 'home/index.view.html',
                controller: 'Home.IndexController',
                controllerAs: 'vm'
            })
            .state('login', {
                url: '/login',
                templateUrl: 'login/index.view.html',
                controller: 'Login.IndexController',
                controllerAs: 'vm'
            });
    }

    function run($rootScope, $http, $location, $localStorage) {
        // keep user logged in after page refresh
        if ($localStorage.currentUser) {
            $http.defaults.headers.common.Authorization = 'Bearer ' + $localStorage.currentUser.token;
        }

        // redirect to login page if not logged in and trying to access a restricted page
        $rootScope.$on('$locationChangeStart', function (event, next, current) {
            var publicPages = ['/login'];
            var restrictedPage = publicPages.indexOf($location.path()) === -1;
            if (restrictedPage && !$localStorage.currentUser) {
                $location.path('/login');
            }
        });
    }
})();
 

AngularJS Main Index File

Path: /index.html

The root index.html file is the main html file for the angular application, it contains the outer template and includes all stylesheets and scripts required by the app.

<!DOCTYPE html>
<html ng-app="app">
<head>
    <title>AngularJS JWT Authentication Example & Tutorial</title>

    <!-- bootstrap css -->
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />

    <!-- application css -->
    <link href="app-content/app.css" rel="stylesheet" />
</head>
<body>
    <!-- main app container -->
    <div class="jumbotron">
        <div class="container">
            <div class="col-sm-8 col-sm-offset-2">
                <ui-view></ui-view>
            </div>
        </div>
    </div>

    <!-- credits -->
    <div class="text-center">
        <p>
            <a href="http://jasonwatmore.com/post/2016/04/05/AngularJS-JWT-Authentication-Example-Tutorial.aspx" target="_top">AngularJS JWT Authentication Example & Tutorial</a>
        </p>
        <p>
            <a href="http://jasonwatmore.com" target="_top">JasonWatmore.com</a>
        </p>
    </div>

    <!-- angular scripts -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-messages.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/ngStorage/0.3.6/ngStorage.min.js"></script>

    <!-- application scripts -->
    <script src="app.js"></script>
    <script src="app-services/authentication.service.js"></script>
    <script src="home/index.controller.js"></script>
    <script src="login/index.controller.js"></script>

    <!-- scripts for fake backend -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-mocks.js"></script>
    <script src="app-helpers/fake-backend.js"></script>
</body>
</html>

 

Recommended Books on AngularJS

To learn more about all the ins and outs of AngularJS I recommend checking out the AngularJS Web Application Development Cookbook by Matt Frisbee, it's full of great real-world examples that are broken down really well by the author.

 

Web Development & AngularJS Consultant Sydney

Feel free to drop me a line if you're looking for a web development or AngularJS consultant in Sydney Australia, I also provide remote contracting services for clients outside Sydney.


Sponsored by