Sunday, December 3, 2017

Json Web Token (JWT) based authentication by using ASP.Net Web API with the help of OWIN middle-ware, ASP .Net Identity and AngularJs Client app. Part 02.

This is the second part of "Json Web Token (JWT) based authentication by using ASP.Net Web API with the help of OWIN middle-ware, ASP .Net Identity and AngularJs Client app. Part 02.", you can find the first part using this link

In this post we will build a Singe Page AngularJS Application named "Client Management Application" where we will display a list of clients.

Now I will explain how this client application communicate with our previously build resource server. By this application user could able to do below functionality.

  1. User could Register to the authorization server by providing username and password.
  2. Allow the user to logged in to the Authorization server and getting the access token for granting access to the secure resource.
  3. View the secure data by providing the bearer access token.

It is required to understand the minimum concept of AngularJS before start working with this client application.

You could download the complete source-code from here.
In this application we are using some third party angular client library.


Steps to prepare Client application:

Step 1:
Add an empty project in the solution named "AngularClientApplication". Folder structure of the app as like below.




































Step 2:
Add Index.html page as it will be the first page when boot the application. We will reference all Javascript and Css libraries to this page.
<!DOCTYPE html>
<html ng-app="angularApp">
<head>
    <title>Angular Client Application</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="Content/bootstrap.css" rel="stylesheet" />
    <link href="Content/animations.css" rel="stylesheet" />
    <link href="Content/styles.css" rel="stylesheet" />
</head>
<body ng-cloak>
    <header class="navbar navbar-inner navbar-fixed-top" ng-controller="navbarController">
        <nav class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" ng-click="isCollapsed = !isCollapsed">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#/">
                    <img src="Content/images/people.png" alt="logo">
                    <span class="appTitle">{{ appTitle }}</span>
                </a>
            </div>
            <div class="nav-container" data-collapse="vm.isCollapsed">
                <ul class="nav navbar-nav nav-pills navBarPadding" menu-highlighter highlight-class-name="active">
                    <li><a href="#/clients">Clients</a></li>
                    <li><a href="#/about">About</a></li>
                    <li data-ng-hide="!authentication.isAuthenticated"><a href="" data-ng-click="logOut()">Logout</a></li>
                    <li data-ng-hide="authentication.isAuthenticated"> <a href="#/login">Login</a></li>
                    <li data-ng-hide="authentication.isAuthenticated">
                        <a href="#/signup">Sign up</a>
                    </li>
                </ul>
            </div>
        </nav>
    </header>
    <div class="slide-animation-container">
        <div ng-view id="ng-view" class="slide-animation"></div>
    </div>
    <div id="footer">
        <div class="navbar navbar-fixed-bottom">
            <div class="navbar-inner footer">
                <div class="container text-center">
                    <footer>
                        <div class="row">
                            <div class="col-md-12">
                                Created by : <a href="https://www.linkedin.com/in/md-towhidul-islam-04513455/" target="_blank">Towhidul Tuhin</a>
                            </div>
                        </div>
                    </footer>
                </div>
            </div>
        </div>
    </div>

    <!-- 3rd party libraries -->
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular-route.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular-animate.min.js"></script>
    <script src="Scripts/lib/angular-ui-bootstrap.js"></script>
    <script src="Scripts/lib/angular-local-storage.min.js"></script>

    <!-- Custom scripts -->
    <script src="Scripts/app/angularApp.js"></script>
    <script src="Scripts/app/navbarController.js"></script>
    <script src="Scripts/app/security/controllers/aboutController.js"></script>
    <script src="Scripts/app/security/controllers/loginController.js"></script>
    <script src="Scripts/app/security/controllers/signupController.js"></script>
    <script src="Scripts/app/security/services/authInterceptorService.js"></script>
    <script src="Scripts/app/security/services/authService.js"></script>

    <script src="Scripts/app/client/controllers/clientsController.js"></script>
    <script src="Scripts/app/client/services/clientsService.js"></script>
</body>
</html>

As you see above html page, angular will be boot by the module "angularApp".

Step 2:
Add angular libraries "angular-local-storage.min.js" and "angular-ui-bootstrap" to the location "Scripts\lib\". angular-local-storage will be used to store the token value in Html 5 local storage.

Step 3:
Add initial js file "angularApp.js" to location "Scripts\app\". This file will consist of "angularApp" module and the route configuration.
var app = angular.module('angularApp', ['ngRoute', 'ui.bootstrap', 'LocalStorageModule']);

app.config(function ($routeProvider) {

    $routeProvider.when("/home", {
        templateUrl: "/Scripts/app/views/home.html"
    });

    $routeProvider.when("/login", {
        controller: "loginController",
        templateUrl: "/Scripts/app/views/login.html"
    });

    $routeProvider.when("/signup", {
        controller: "signupController",
        templateUrl: "/Scripts/app/views/signup.html"
    });

    $routeProvider.when("/clients", {
        controller: "clientsController",
        templateUrl: "/Scripts/app/views/clients.html"
    });

    $routeProvider.when("/about", {
        controller: "aboutController",
        templateUrl: "/Scripts/app/views/about.html"
    });

    $routeProvider.otherwise({ redirectTo: "/home" });

});

var serviceBase = 'http://localhost:59598/';
app.constant('ngAuthSettings', {
    apiServiceBaseUri: serviceBase,
    clientId: 'IAmTheFirstClient'
});

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

app.run(['authService', function (authService) {
    authService.fillAuthData();
}]);
Above code-block holds all route configuration and a constant value "ngAuthSettings" which hold our authorization server base url and client id. One important things is configured here which is angular http interceptor. This interceptor is responsible to configure a XHR request before sending the request to the server.

Set 4:
Add a controller "navbarController.js" to the location "Scripts\app\". This controller will hold the Login and Log out function.
(function () {

    var injectParams = ['$scope', '$location', 'authService'];

    var NavbarController = function ($scope, $location, authService) {
            $scope.appTitle = 'Client Management';
            $scope.authentication = authService.authentication;

        $scope.logOut = function () {
                var isAuthenticated = authService.authentication.isAuthenticated;
                if (isAuthenticated) { //logout 
                    authService.logOut();
                    $location.path('/');
                    return;
                }
            };

        $scope.login = function () {
            var path = '/login';
            $location.replace();
            $location.path(path);
        };

    };

    NavbarController.$inject = injectParams;

    angular.module('angularApp').controller('navbarController', NavbarController);

}());

Step 5:
Add a factory service "authInterceptorService.js" to the location "Scripts\app\security\services". 
'use strict';
app.factory('authInterceptorService', ['$q', '$injector','$location', 'localStorageService', function ($q, $injector,$location, localStorageService) {

    var authInterceptorServiceFactory = {};

    var _request = function (config) {

        config.headers = config.headers || {};
       
        var authData = localStorageService.get('authorizationData');
        if (authData) {
            config.headers.Authorization = 'Bearer ' + authData.token;
        }

        return config;
    }

    var _responseError = function (rejection) {
        if (rejection.status === 401) {
            var authService = $injector.get('authService');
            var authData = localStorageService.get('authorizationData');

            if (authData) {
                if (authData.useRefreshTokens) {
                    $location.path('/refresh');
                    return $q.reject(rejection);
                }
            }
            authService.logOut();
            $location.path('/login');
        }
        return $q.reject(rejection);
    }

    authInterceptorServiceFactory.request = _request;
    authInterceptorServiceFactory.responseError = _responseError;

    return authInterceptorServiceFactory;
}]);
This service will be responsible to inject some configuration to the XHR request before going it to the actual server.

Step 6:
Add another factory service "authService.js" to the same folder mentioned above. This service will be responsible to API call like user registration, token generation, store the token in local storage after validation.
'use strict';
app.factory('authService', ['$http', '$q', 'localStorageService', 'ngAuthSettings', function ($http, $q, localStorageService, ngAuthSettings) {

    var serviceBase = ngAuthSettings.apiServiceBaseUri;
    var authServiceFactory = {};

    var _authentication = {
        isAuthenticated: false,
        userName: "",
        useRefreshTokens: false
    };

    var _saveRegistration = function (registration) {

        _logOut();

        return $http.post(serviceBase + 'api/account/register', registration).then(function (response) {
            return response;
        });

    };

    var _login = function (loginData) {

        var data = "grant_type=password&username=" + loginData.userName + "&password=" + loginData.password + "&client_id=" + ngAuthSettings.clientId;
        
        var deferred = $q.defer();

        $http.post(serviceBase + 'security/token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).success(function (response) {

            if (loginData.useRefreshTokens) {
                localStorageService.set('authorizationData', { token: response.access_token, userName: loginData.userName, refreshToken: response.refresh_token, useRefreshTokens: true });
            }
            else {
                localStorageService.set('authorizationData', { token: response.access_token, userName: loginData.userName, refreshToken: "", useRefreshTokens: false });
            }
            _authentication.isAuthenticated = true;
            _authentication.userName = loginData.userName;
            _authentication.useRefreshTokens = loginData.useRefreshTokens;

            deferred.resolve(response);

        }).error(function (err, status) {
            _logOut();
            deferred.reject(err);
        });

        return deferred.promise;

    };

    var _logOut = function () {

        localStorageService.remove('authorizationData');

        _authentication.isAuthenticated = false;
        _authentication.userName = "";
        _authentication.useRefreshTokens = false;

    };

    var _fillAuthData = function () {

        var authData = localStorageService.get('authorizationData');
        if (authData) {
            _authentication.isAuthenticated = true;
            _authentication.userName = authData.userName;
            _authentication.useRefreshTokens = authData.useRefreshTokens;
        }

    };

    authServiceFactory.saveRegistration = _saveRegistration;
    authServiceFactory.login = _login;
    authServiceFactory.logOut = _logOut;
    authServiceFactory.fillAuthData = _fillAuthData;
    authServiceFactory.authentication = _authentication;

    return authServiceFactory;
}]);

Step 7:
Add "signupController.js" controller to the location "Scripts\app\security\controllers". This controller will be responsible to register an User.
'use strict';
app.controller('signupController', ['$scope', '$location', 'authService', function ($scope, $location, authService) {

    $scope.savedSuccessfully = false;
    $scope.message = "";

    $scope.registration = {
        name: "",
        password: "",
        confirmPassword: ""
    };

    $scope.signUp = function () {

        authService.saveRegistration($scope.registration).then(function (response) {

            $scope.savedSuccessfully = true;
            $scope.message = "User has been registered successfully";
            $location.path('/login');

        },
         function (response) {
             var errors = [];
             for (var key in response.data.modelState) {
                 for (var i = 0; i < response.data.modelState[key].length; i++) {
                     errors.push(response.data.modelState[key][i]);
                 }
             }
             $scope.message = "Failed to register user due to:" + errors.join(' ');
         });
    };

}]);

Step 8:
Add "loginController.js" controller to the location "Scripts\app\security\controllers". This controller will responsible to login the user to the system.
'use strict';
app.controller('loginController', ['$scope', '$location', 'authService', 'ngAuthSettings', function ($scope, $location, authService, ngAuthSettings) {

    $scope.loginData = {
        userName: "",
        password: "",
        useRefreshTokens: false
    };

    $scope.message = "";

    $scope.login = function () {

        authService.login($scope.loginData).then(function (response) {
            
            $location.path('/clients');

        },
         function (err) {
             $scope.message = err.error_description;
         });
    };
}]);

Step 9:
Add "clientsController.js" controller to the location "Scripts\app\client\controllers". This controller will be responsible for calling a getClients sevice.
'use strict';
app.controller('clientsController', ['$scope', 'clientsService', function ($scope, clientsService) {

    $scope.clients = [];

    clientsService.getClients().then(function (results) {

        $scope.clients = results.data;

    }, function (error) {
    });

}]);
Step 10:
Add "clientsService.js" controller to the location "Scripts\app\client\services". This service is responsible for retrieve client data from the resource serve by a XHR request.
'use strict';
app.factory('clientsService', ['$http', 'ngAuthSettings', function ($http, ngAuthSettings) {

    var serviceBase = ngAuthSettings.apiServiceBaseUri;

    var clientsServiceFactory = {};

    var _getClients = function () {

        return $http.get(serviceBase + 'api/clients').then(function (results) {
            return results;
        });
    };

    clientsServiceFactory.getClients = _getClients;

    return clientsServiceFactory;

}]);

Step 11:
Add view "signup.html" to the location "Scripts\app\views". This view will render for register an user.
<div class="view">
    <div class="container">
        <header>
            <h3><span class="glyphicon glyphicon-lock"></span> Sign up</h3>
        </header>
        <form name="loginForm" novalidate>
            <div class="login">
                <div class="row">
                    <div class="col-md-2">
                        User Name:
                    </div>
                    <div class="col-md-10">
                        <input type="text" name="userName" class="form-control" data-ng-model="registration.name" required />
                        <span class="errorMessage" ng-show="loginForm.userName.$touched && loginForm.userName.$invalid">
                            User Name is required
                        </span>
                    </div>
                </div>
                <br />
                <div class="row">
                    <div class="col-md-2">
                        Password:
                    </div>
                    <div class="col-md-10">
                        <input type="password" name="password" class="form-control"
                               data-ng-model="registration.password"
                               data-ng-minlength="6"
                               required />
                        <span class="errorMessage" ng-show="loginForm.password.$touched && loginForm.password.$invalid">
                            Password is required
                        </span>
                    </div>
                </div>
                <br />
                <div class="row">
                    <div class="col-md-2">
                        Confirm Password:
                    </div>
                    <div class="col-md-10">
                        <input type="password" name="confirmPassword" class="form-control"
                               data-ng-model="registration.confirmPassword"
                               data-ng-minlength="6"
                               required />
                        <span class="errorMessage" ng-show="loginForm.password.$touched && loginForm.password.$invalid">
                            Confirm password is required
                        </span>
                    </div>
                </div>
                <br />
                <div class="row">
                    <div class="col-md-12">
                        <button type="submit" class="btn btn-primary"
                                data-ng-click="signUp()"
                                ng-disabled="loginForm.$invalid || !loginForm.$dirty">
                            Save
                        </button>
                    </div>
                </div>
                <br />
                <div class="statusRow">
                    <br />
                    <div class="label label-important" data-ng-show="errorMessage">
                        <span class="glyphicon glyphicon-thumbs-down icon-white"></span>&nbsp;&nbsp;Error: {{ errorMessage }}
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>
Step 12:
Add view "login.html" to the location "Scripts\app\views". This view will render for login an user to the system.
<div class="view">
    <div class="container">
        <header>
            <h3><span class="glyphicon glyphicon-lock"></span> Login</h3>
        </header>
        <form name="loginForm" novalidate>
            <div class="login">
                <div class="row">
                    <div class="col-md-2">
                        User Name:
                    </div>
                    <div class="col-md-10">
                        <input type="text" name="userName" class="form-control" data-ng-model="loginData.userName" required />
                        <span class="errorMessage" ng-show="loginForm.userName.$touched && loginForm.userName.$invalid">
                            User Name is required
                        </span>
                    </div>
                </div>
                <br />
                <div class="row">
                    <div class="col-md-2">
                        Password:
                    </div>
                    <div class="col-md-10">
                        <input type="password" name="password" class="form-control"
                               data-ng-model="loginData.password"
                               data-ng-minlength="6"
                               required />
                        <span class="errorMessage" ng-show="loginForm.password.$touched && loginForm.password.$invalid">
                            Password is required
                        </span>
                    </div>
                </div>
                <br />
                <div class="row">
                    <div class="col-md-2">
                    </div>
                    <div class="col-md-10">
                        <button type="submit" class="btn btn-primary"
                                data-ng-click="login()"
                                ng-disabled="loginForm.$invalid || !loginForm.$dirty">
                            Login
                        </button>
                        <a href="/#/signup">Sign up</a>
                    </div>
                </div>
                <br />
                <div class="statusRow">
                    <br />
                    <div class="label label-important" data-ng-show="errorMessage">
                        <span class="glyphicon glyphicon-thumbs-down icon-white"></span>&nbsp;&nbsp;Error: {{ errorMessage }}
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>
Step 13:
Add view "home.html" to the location "Scripts\app\views". This view contain a placeholder for home page.
    <div class="row">
        <div class="jumbotron">
            <div class="container">
                <div class="page-header text-center">
                    <h1>Json Web Token(JWT) base Authentication</h1>
                </div>
                <p>REST Web API Application which is responsible for generating OAuth Bearer Token(JWT) and authentication and authorization. This backend API is fully stateless and built using ASP.NET Web API 2, OWIN middleware, and ASP.NET Identity. AngularJs as the client application.</p>
            </div>
        </div>
    </div>
Step 14:
Add view "clients.html" to the location "Scripts\app\views". This view render for showing list of clients.
<div class="clients view indent">
    <div class="container">
        <header>
            <h3><span class="glyphicon glyphicon-user"></span> clients</h3>
        </header>
        <div class="container">
            <div class="row gridContainer clientsTable show-hide-animation">
                <div class="col-md-10">
                    <div>
                        <table class="table table-striped table-condensed">
                            <thead>
                                <tr>
                                    <th>&nbsp;</th>
                                    <th>Name</th>
                                    <th>Regional Country</th>
                                    <th>Address</th>
                                    <th>Projects</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr data-ng-repeat="client in clients | orderBy:Name" class="repeat-animation">
                                    <td><img data-ng-src="Content/images/{{client.Name | lowercase}}.png" class="cardImage" alt="Client Image" /></td>
                                    <td>{{client.Name}}</td>
                                    <td>{{client.RegionalCountry}}</td>
                                    <td>{{client.Address}}</td>
                                    <td>
                                        <a href="#/clients" class="btn-link">
                                            {{ client.ProjectCount }}
                                            <span data-ng-pluralize count="client.ProjectCount"
                                                  when="{'1': 'Project','other': 'Projects'}">
                                            </span>
                                        </a>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
        <div class="container">
            <div class="row show-hide-animation" data-ng-show="filteredclients.length == 0">
                <div class="col-span-12">
                    <h4>No clients found</h4>
                </div>
            </div>
        </div>
        <div class="row show-hide-animation" data-ng-show="clients.length > 0">
            <div class="col-md-12">
                <div data-pagination
                     data-on-select-page="pageChanged(page)"
                     data-total-items="totalRecords"
                     data-page="currentPage"
                     data-items-per-page="pageSize"
                     data-boundary-links="true"
                     class="pagination-sm"></div>
            </div>
        </div>
        <br /><br /><br />
    </div>
</div>

Step 15:
You need to add the style.css as your own to show the page perfect looking.

Now we are done and ready to use the client application. It's time to run both application and test it.

By the help of visual studio we could run both application at a time by following the instruction.
  1. Right click on the solution and click Properties.
  2. From the left menu select "Startup Project"
  3. On the right side select "Multiple startup projects"
  4. On action tab select "Start" for both projects. You could do this by the help of below image



Now it's time to play with the applications.
Hopefully you enjoyed the post.

Any feedback or comments would be appreciable,  if there is a better way or modified way to implement this.

No comments:

Post a Comment

A Deep Dive into Computed Columns in Entity Framework Core

Entity Framework Core (EF Core) is a popular Object-Relational Mapping (ORM) framework that simplifies database access for .NET applications...