Part
2
AngularJS
Token Authentication using ASP.NET Web API 2, Owin, and Identity
This
is the second part of AngularJS Token Authentication using ASP.NET
Web API 2 and Owin middleware, you can find the first part using the
link below:
You
can check the demo
application on
(http://ngAuthenticationWeb.azurewebsites.net),
play with the back-end API for learning purposes
(http://ngauthenticationapi.azurewebsites.net),
and check the source
code on Github.
In
this post we’ll build sample SPA using AngularJS, this application
will allow the users to do the following:
- Register in our system by providing username and password.
- Secure certain views from viewing by authenticated users (Anonymous users).
- Allow registered users to log-in and keep them logged in for 30 minutes because we are using refresh tokens or until they log-out from the system, this should be done using tokens.
If
you are new to AngularJS, you can check my other
tutorial which provides step by step instructions on how to build
SPA using AngularJS, it is important to understand the fundamentals
aspects of AngularJS before start working with it, in this tutorial
I’ll assume that reader have basic understanding of how
AngularJS works.
Step 1: Download Third Party Libraries
To
get started we need to download all libraries needed in our
application:
- AngularJS: We’ll serve AngularJS from from CDN, the version is 1.2.16
- Loading Bar: We’ll use the loading bar as UI indication for every XHR request the application will made, to get this plugin we need to download it from here.
- UI Bootstrap theme: to style our application, we need to download a free bootstrap ready made theme from http://bootswatch.com/ I’ve used a theme named “Yeti”.
Step 2: Organize Project Structure
You
can use your favorite IDE to build the web application, the app is
completely decoupled from the back-end API, there is no dependency on
any server side technology here, in my case I’m using Visual Studio
2013 so add new project named “AngularJSAuthentication.Web” to
the solution we created in the previous
post, the template for this project is “Empty” without any
core dependencies checked.
After
you add the project you can organize your project structure as the
image below, I prefer to contain all the AngularJS
application and resources files we’ll create in folder named “app”.
Step 3: Add the Shell Page (index.html)
Now
we’ll add the “Single Page” which is a container for our
application, it will contain the navigation menu and AngularJS
directive for rendering different application views “pages”.
After you add the “index.html” page to project root we need to
reference the 3rd party JavaScript and CSS files needed as the below:
Click To Expand Code 1Step 4: “Booting up” our Application and Configure Routes
We’ll
add file named “app.js” in the root of folder “app”, this
file is responsible to create modules in applications, in our case
we’ll have a single module called “AngularAuthApp”, we can
consider the module as a collection of services, directives,
filters which is used in the application. Each module has
configuration block where it gets applied to the application during
the bootstrap process.
As
well we need to define and map the views with the controllers so open
“app.js” file and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
var app = angular.module('AngularAuthApp', ['ngRoute',
'LocalStorageModule', 'angular-loading-bar']); app.config(function ($routeProvider) { $routeProvider.when("/home", { controller: "homeController", templateUrl: "/app/views/home.html" }); $routeProvider.when("/login", { controller: "loginController", templateUrl: "/app/views/login.html" }); $routeProvider.when("/signup", { controller: "signupController", templateUrl: "/app/views/signup.html" }); $routeProvider.when("/orders", { controller: "ordersController", templateUrl: "/app/views/orders.html" }); $routeProvider.otherwise({ redirectTo: "/home" }); }); app.run(['authService', function (authService) { authService.fillAuthData(); }]); |
So
far we’ve defined and mapped 4 views to their corresponding
controllers as the below:
- Home view which shows the home page and can be accessed by anonymous users on http://ngauthenticationweb.azurewebsites.net/#/home
- Signup view which shows signup form and can be accessed by anonymous users on http://ngauthenticationweb.azurewebsites.net/#/signup
- Log-in view which shows log-in form and can be accessed by anonymous users on http://ngauthenticationweb.azurewebsites.net/#/login
- Orders view which shows orders forms for authenticated users only as the image below, view can be accessed on http://ngauthenticationweb.azurewebsites.net/#/orders
Step 5: Add AngularJS Authentication Service (Factory)
This
AngularJS service will be responsible for signing up new users,
log-in/log-out registered users, and store the generated token in
client local storage so this token can be sent with each request to
access secure resources on the back-end API, the code for AuthService
will be as the below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
'use strict'; app.factory('authService', ['$http', '$q', 'localStorageService', function ($http, $q, localStorageService) { var serviceBase = 'http://ngauthenticationapi.azurewebsites.net/'; var authServiceFactory = {}; var _authentication = { isAuth: false, userName : "" }; 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; var deferred = $q.defer(); $http.post(serviceBase + 'token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).success(function (response) { localStorageService.set('authorizationData', { token: response.access_token, userName: loginData.userName }); _authentication.isAuth = true; _authentication.userName = loginData.userName; deferred.resolve(response); }).error(function (err, status) { _logOut(); deferred.reject(err); }); return deferred.promise; }; var _logOut = function () { localStorageService.remove('authorizationData'); _authentication.isAuth = false; _authentication.userName = ""; }; var _fillAuthData = function () { var authData = localStorageService.get('authorizationData'); if (authData) { _authentication.isAuth = true; _authentication.userName = authData.userName; } } authServiceFactory.saveRegistration = _saveRegistration; authServiceFactory.login = _login; authServiceFactory.logOut = _logOut; authServiceFactory.fillAuthData = _fillAuthData; authServiceFactory.authentication = _authentication; return authServiceFactory; }]); |
Now
by looking on the method “_saveRegistration” you will notice
that we are issuing HTTP Post to the end point
“http://ngauthenticationapi.azurewebsites.net/api/account/register”
defined in the previous
post, this method returns a promise which will be resolved in the
controller.
The
function “_login” is responsible to send HTTP Post request
to the endpoint “http://ngauthenticationapi.azurewebsites.net/token”,
this endpoint will validate the credentials passed and if they are
valid it will return an “access_token”. We have to store this
token into persistence medium on the client so for any subsequent
requests for secured resources we’ve to read this token value and
send it in the “Authorization” header with the HTTP request.
Notice
that we have configured the POST request for this endpoint to
use “application/x-www-form-urlencoded” as its Content-Type and
sent the data as string not JSON object.
The
best way to store this token is to use AngularJS module named
“angular-local-storage”
which gives access to the browsers local storage with cookie
fallback if you are using old browser, so I will depend on this
module to store the token and the logged in username in key named
“authorizationData”. We will use this key in different
places in our app to read the token value from it.
As
well we’ll add object named “authentication” which will store
two values (isAuth, and username). This object will be used to change
the layout for our index page.
Step 6: Add the Signup Controller and its View
The
view for the signup is simple so open file named “signup.html”
and add it under folders “views” open the file and paste the HTML
below:
1 2 3 4 5 6 7 8 9 10 |
<form class="form-login" role="form"> <h2 class="form-login-heading">Sign up</h2> <input type="text" class="form-control" placeholder="Username" data-ng-model="registration.userName" required autofocus> <input type="password" class="form-control" placeholder="Password" data-ng-model="registration.password" required> <input type="password" class="form-control" placeholder="Confirm Password" data-ng-model="registration.confirmPassword" required> <button class="btn btn-lg btn-info btn-block" type="submit" data-ng-click="signUp()">Submit</button> <div data-ng-hide="message == ''" data-ng-class="(savedSuccessfully) ? 'alert alert-success' : 'alert alert-danger'"> {{message}} </div> </form> |
Now
we need to add controller named “signupController.js” under
folder “controllers”, this controller is simple and will contain
the business logic needed to register new users and call the
“saveRegistration” method we’ve created in “authService”
service, so open the file and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
'use strict'; app.controller('signupController', ['$scope', '$location', '$timeout', 'authService', function ($scope, $location, $timeout, authService) { $scope.savedSuccessfully = false; $scope.message = ""; $scope.registration = { userName: "", password: "", confirmPassword: "" }; $scope.signUp = function () { authService.saveRegistration($scope.registration).then(function (response) { $scope.savedSuccessfully = true; $scope.message = "User has been registered successfully, you will be redicted to login page in 2 seconds."; startTimer(); }, 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(' '); }); }; var startTimer = function () { var timer = $timeout(function () { $timeout.cancel(timer); $location.path('/login'); }, 2000); } }]); |
Step 6: Add the log-in Controller and its View
The
view for the log-in is simple so open file named “login.html”
and add it under folders “views” open the file and paste
the HTML below:
1 2 3 4 5 6 7 8 9 |
<form class="form-login" role="form"> <h2 class="form-login-heading">Login</h2> <input type="text" class="form-control" placeholder="Username" data-ng-model="loginData.userName" required autofocus> <input type="password" class="form-control" placeholder="Password" data-ng-model="loginData.password" required> <button class="btn btn-lg btn-info btn-block" type="submit" data-ng-click="login()">Login</button> <div data-ng-hide="message == ''" class="alert alert-danger"> {{message}} </div> </form> |
Now
we need to add controller named “loginController.js” under folder
“controllers”, this controller will be responsible to redirect
authenticated users only to the orders view, if you tried to
request the orders view as anonymous user, you will be
redirected to log-in view. We’ll see in the next steps how we’ll
implement the redirection for anonymous users to the log-in view
once users request a secure view.
Now
open the “loginController.js” file and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
'use strict'; app.controller('loginController', ['$scope', '$location', 'authService', function ($scope, $location, authService) { $scope.loginData = { userName: "", password: "" }; $scope.message = ""; $scope.login = function () { authService.login($scope.loginData).then(function (response) { $location.path('/orders'); }, function (err) { $scope.message = err.error_description; }); }; }]); |
Step 7: Add AngularJS Orders Service (Factory)
This
service will be responsible to issue HTTP GET request to the end
point “http://ngauthenticationapi.azurewebsites.net/api/orders”
we’ve defined in the previous
post, if you recall we added “Authorize” attribute to
indicate that this method is secured and should be called by
authenticated users, if you try to call the end point directly
you will receive HTTP status code 401 Unauthorized.
So
add new file named “ordersService.js” under folder “services”
and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
'use strict'; app.factory('ordersService', ['$http', function ($http) { var serviceBase = 'http://ngauthenticationapi.azurewebsites.net/'; var ordersServiceFactory = {}; var _getOrders = function () { return $http.get(serviceBase + 'api/orders').then(function (results) { return results; }); }; ordersServiceFactory.getOrders = _getOrders; return ordersServiceFactory; }]); |
By
looking at the code above you’ll notice that we are not setting
the “Authorization” header and passing the bearer token we
stored in the local storage earlier in this service, so we’ll
receive 401 response always! Also we are not checking if the
response is rejected with status code 401 so we redirect the user to
the log-in page.
There
is nothing prevent us from reading the stored token from
the local storage and checking if the response is rejected
inside this service, but what if we have another services that
needs to pass the bearer token along with each request? We’ll end
up replicating this code for each service.
To
solve this issue we need to find a centralized place so we add
this code once so all other services interested in
sending bearer token can benefit from it, to do so we need to
use “AngualrJS Interceptor“.
Step 8: Add AngularJS Interceptor (Factory)
Interceptor
is regular service (factory) which allow us to capture every XHR
request and manipulate it before sending it to the back-end API or
after receiving the response from the API, in our case we are
interested to capture each request before sending it so we can
set the bearer token, as well we are interested in checking if the
response from back-end API contains errors which means we need to
check the error code returned so if its 401 then we redirect the user
to the log-in page.
To
do so add new file named “authInterceptorService.js” under
“services” folder and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
'use strict'; app.factory('authInterceptorService', ['$q', '$location', 'localStorageService', function ($q, $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) { $location.path('/login'); } return $q.reject(rejection); } authInterceptorServiceFactory.request = _request; authInterceptorServiceFactory.responseError = _responseError; return authInterceptorServiceFactory; }]); |
By
looking at the code above, the method “_request” will be fired
before $http sends the request to the back-end API, so this is the
right place to read the token from local storage and set it into
“Authorization” header with each request. Note that I’m
checking if the local storage object is nothing so in this case this
means the user is anonymous and there is no need to set the token
with each XHR request.
Now
the method “_responseError” will be hit after the we receive a
response from the Back-end API and only if there is failure status
returned. So we need to check the status code, in case it was 401
we’ll redirect the user to the log-in page where he’ll be able to
authenticate again.
Now
we need to push this interceptor to the interceptors array, so
open file “app.js” and add the below code snippet:
1 2 3 |
app.config(function ($httpProvider) { $httpProvider.interceptors.push('authInterceptorService'); }); |
By
doing this there is no need to setup extra code for setting up
tokens or checking the status code, any AngularJS service executes
XHR requests will use this interceptor. Note: this will work if you
are using AngularJS service $http or $resource.
Step 9: Add the Index Controller
Now
we’ll add the Index controller which will be responsible to
change the layout for home page i.e (Display Welcome {Logged In
Username}, Show My Orders Tab), as well we’ll add log-out
functionality on it as the image below.
Taking
in consideration that there is no straight way to log-out the user
when we use token based approach, the work around we can do here is
to remove the local storage key “authorizationData” and set
some variables to their initial state.
So
add a file named “indexController.js” under folder
“controllers” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 |
'use strict'; app.controller('indexController', ['$scope', '$location', 'authService', function ($scope, $location, authService) { $scope.logOut = function () { authService.logOut(); $location.path('/home'); } $scope.authentication = authService.authentication; }]); |
Step 10: Add the Home Controller and its View
This
is last controller and view we’ll add to complete the app, it is
simple view and empty controller which is used to display two boxes
for log-in and signup as the image below:
So
add new file named “homeController.js” under the “controllers”
folder and paste the code below:
1 2 3 4 |
'use strict'; app.controller('homeController', ['$scope', function ($scope) { }]); |
As
well add new file named “home.html” under “views” folder and
paste the code below:
Click To Expand Code
By
now we should have SPA which uses the token based approach to
authenticate users.
One
side note before closing: The redirection for anonymous users to
log-in page is done on client side code; so any malicious user
can tamper with this. It is very important to secure all back-end
APIs as we implemented on this tutorial and not to depend on client
side code only.
That’s
it for now! Hopefully this two posts will be beneficial for folks
looking to use token based authentication along with ASP.NET Web API
2 and Owin middleware.
I
would like to hear your feedback and comments if there is a better
way to implement this especially redirection users to log-in page
when the are anonymous.
You
can check the demo
application on
(http://ngAuthenticationWeb.azurewebsites.net),
play with the back-end API for learning purposes
(http://ngauthenticationapi.azurewebsites.net),
and check the source code
on Github.