1. David Antaramian
  2. SO-21662778 SPA Authentication Example

Overview

HTTPS SSH

This project is a tutorial presented as a detailed answer for a question posed by Samantha J. on StackOverflow.

The question, at time of development, reads as follows:

I would like to create a new AngularJS, Web API Single page application. Does anyone have any examples that show how I can set up a user login screen that connects to a WEB API controller for a simple login (no need for google/facebook login etc) that uses ASP.NET Identity and without the need for user registration.

Also how can I handle showing a new view once the login has been completed. What I would like is to have a solution that does not show routing in the browser URL. So for example I would like to be able to switch from the login view and a couple of other different views without the url changing from www.abc.com.

In other words I would like to avoid showing www.abc.com/login, www.abc.com/screen1, www.abc.com/screen2

Any advice would be much appreciated.

I have done my best to conform to the specifications listed above and then some, but I have included some notes on my reservations, especially about the lack of exposing the URL routing scheme.

About the Solution

The way I have crafted this solution is to segregate the data service (ASP.NET Web API 2) from the data view (HTML5 + AngularJS). For the data service, I am using Visual Studio Express 2013 for Web with NuGet to provide scaffolding, package dependency resolution, and build tools. For the data view, I am using Yeoman (providing Grunt and Bower) to provide scaffolding, package dependency resolution, and build tools. This in no way implies that this is the way to do it; it just happens to be the way that I do it.

Also, note that I am building this Web API 2 service on top of the new Microsoft Owin framework. I could have built it directly on top of ASP.NET, but the internal OAuth 2.0 bearer token authentication flow for the ASP.NET Identity system is built on top of Owin, and I would rather implement everything in the same primary pipeline. I am adding CORS support to the application so that I can host my data service at api.example.com and my data data view at www.example.com. CORS support should be set up specific to your applications needs. Globally supporting any incoming domain is a bad idea unless your specifications state that anyone should be able to consume the API.

On the web view side, I will not be using the normal $ngRoute service provided by AngularJS. The specifications of this example require it to only have a single route ("/"). This requires all the "routing" to be done through a state machine which is provided by AngularUI Router.

Credit Where Due

I have to thank Sedushi for providing his Plunker code illustrating his use of $locationProvider.html5Mode(true) and $stateProvider. I also have to thank Bruno Scopelliti for his blog posts on AngularJS authentication methods.

The Web API 2 Layer

Step 01: Start A New Solution

In my case, I've named it "SPA Authentication Example." I'm using Visual Studio Express for Web 2013, and I've specifically selected "Empty Project." If I selected "Web API" or "Single Page Application," the project template is going to cram MVC dependencies down my throat and KnockoutJS on top of that if I choose the SPA template. I really don't want either of those for this project, as noted above. The downside is that it will not give you authentication options. I'll manually add those later. So, now we have a basic project.

View of the Visual Studio 2013 template selection screen

These are all the changes made in this step.

Step 02: Do Any Set-Up Work

For me, this was just changing the default namespace to something more palatable and then performing a refactor operation on the codebase. I also enabled NuGet Package Restore so that the dependencies aren't included wholesale in my source code tree. These are personal preferences, so season to taste.

These are all the changes made in this step.

Step 03: Install Dependencies

We will use NuGet to manage dependencies, so install the following using the package manager console:

Install-Package Microsoft.AspNet.Identity.Owin
Install-Package Microsoft.AspNet.Identity.EntityFramework
Install-Package Microsoft.Owin.Cors
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.AspNet.WebApi.Owin

I chose these packages specifically for their depedency chain cascade. After installing the above packages, you'll have everything you'll need for this project. I also took the chance to pull in the latest updates for dependencies.

You'll want to specifically look at the changes to packages.config for this step as well as to Web.config.

These are all the changes made in this step.

Step 04: Delete Global.asax

We don't actually need a Global.asax (or Global.asax.cs for that matter) because everything is going to be passed down to the OWIN pipeline.

These are all the changes made in this step.

Step 05: Create an OWIN Startup Class

Create a new file Startup.cs at the root of your project. Replace the generated code with the following code:

using System.Web.Http;
using Owin;
using Microsoft.Owin;
using Microsoft.Owin.Cors;

[assembly: OwinStartup(typeof(Antaramian.SPAAuthenticationExample.Startup))]

namespace Antaramian.SPAAuthenticationExample
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();
            WebApiConfig.Register(config);
            app.UseCors(CorsOptions.AllowAll);
            app.UseWebApi(config);
        }
    }
}

We need to include the Owin depdency in this file because that's what includes the IAppBuilder interface. This interface is designed as a specification of the OWIN application middleware. The Microsoft.Owin dependency provides the actual implementation of this interface. We also include Microsoft.Owin.Cors to define our CORS policy which will be applied to any request. You'll see later how the authentication framework inherits this policy.

The assembly attribute directs Microsoft.Owin as to which Startup class we want to use for our application. In this case, it's the class we are creating, Startup. The class itself is partial because we will be defining more of it later in a file that deals specifically with authentication.

The only method that the Startup class contains now is Configuration which takes a parameter of the interface type IAppBuiler, so any object implementing that interface can be passed in. This parameter will be supplied by the host at runtime.

We now add an HttpConfiguration which we will really only be using for the purposes of route mapping. We pass the config object to WebApiConfig.Register() to handle the routing. That code was defined by the project template in App_Start\WebApiConfig.cs, but it reads as follows for those who don't have it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace Antaramian.SPAAuthenticationExample
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

The next line tells the app that we will be specifying a CORS policy. In this case, I am using the pre-defined AllowAll definition. This is only for testing purposes. In a production situation, you would typically create a new policy provider that determines the specific incoming HTTP origins, headers, and methods you expect.

Then we pass the config object to a method call on the app object called UseWebApi() which is just telling the OWIN middleware to wire-up the ASP.NET Web API 2 framework to its pipeline.

We now have an app that works! It doesn't do anything, but it sure does compile.

These are all the changes made in this step.

Step 06: Setting Up A Database

Now, before adding the authentication code, we'll need something to authenticate against. In the Models folder, create a new class called ExampleContext. Replace it with the following code:

using Microsoft.AspNet.Identity.EntityFramework;

namespace Antaramian.SPAAuthenticationExample.Models
{
    public class ExampleContext : IdentityDbContext<IdentityUser>
    {
        public ExampleContext() : base("ExampleContext")
        {
        }
    }
}

The key here is that our context is inheriting from the IdentityDbContext. We're also giving IdentityDbContext a TUser type of IdentityUser. IdentityUser is the basic user model included with ASP.NET Identity. You could create a new model that extends IdentityUser and add domain specific data to it. We don't need any of that here, though.

Now in your Web.config file, add something along the lines of the following to your <configuration> section:

<connectionStrings>
    <add name="ExampleContext" connectionString="Data Source=(LocalDB)\v11.0;Initial Catalog=SPAAuthenticationExample;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>

And add lines of the following nature to the <entityFramework> section:

<contexts>
    <context type="Antaramian.SPAAuthenticationExample.Models.ExampleContext, Antaramian.SPAAuthenticationExample" />
</contexts>

Your changes should look along the lines of this. Your actual entries will vary depending on your data source and namespace. Now in the package manager console, enter the following:

Enable-Migrations

This will enable migrations on the project so that EntityFramework can migrate the database as the domain model evolves. It will also allow us to create seed data. In order to properly migrate the database, though, Entity Framework needs to make a snapshot of the data-model. Enter the following in the package manager console:

Add-Migration Initial

You can close the file it opens. It only shows the migration code. Instead, open the new Configuration.cs file in the Migrations folder. This is where we can seed the database with data. It will run everytime the database is migrated upwards, so it's important that the code is aware of whether something exists in the database already or not. Replace what is currently there with:

namespace Antaramian.SPAAuthenticationExample.Migrations
{
    using System.Data.Entity.Migrations;
    using Antaramian.SPAAuthenticationExample.Models;
    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.EntityFramework;

    internal sealed class Configuration : DbMigrationsConfiguration<ExampleContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        private bool AddUser(ExampleContext context)
        {
            IdentityResult identityResult;
            UserManager<IdentityUser> userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(context));
            var user = new IdentityUser() {
              UserName = "admin"
            };
            if(userManager.FindByName(user.UserName) != null) {
                return true;
            }
            identityResult = userManager.Create(user, "password");
            return identityResult.Succeeded;
        }
        protected override void Seed(ExampleContext context)
        {
            AddUser(context);
        }
    }
}

So that's a lot of code for very little results. It does include some administrative overhead though. First of all, the constructor is specifying that automatic migrations are off, so we have to explicitly create migrations via the package manager console before calling Update-Database will actually make a change. I then define a private function that returns a bool. The return value isn't actually used for anything, but you could use it later if you, for example, need to make sure that the user set was created before seeding more data.

The AddUserAndRole() function creates a new IdentityResult object which will be used to monitor the outcome of Identity related actions. It then creates a UserManager<> object passing in IdentityUser as the TUser type. As above in the ExampleContext class, you would change this to be the name of whatever model class you use to extend IdentityUser if you want to add more application-specific data to the model. In the constructor call for the UserManager object, we create a new inline intance of the UserStore object, again passing IdentityUser as the TUser type. The constructor for the UserStore object takes the database access context as its argument, so we pass in the context which is in turn passed to AddUserAndRole() from the Seed() method.

Now we can create a new user. The IdentityUser model is very sparse, so all we really get to define is the username. In this case we will user admin. Next we check if the user already exists and return true if so. (We'll assume that the return value is a check on the existence of users and not on the ability to create them.) If the user doesn't exist, we will call the UserManager.Create() method. The method takes the IdentityUser (or derivative) object as well as a plaintext password and returns an IdentityResult. Here we are just setting our user's password to password. We take the IdentityResult it returns and return the value of its Succeeded parameter to the caller.

The Seed() method will be called by the migration functions, so it's important that we put the call to AddUserAndRole() in the function body in order for our custom function to actually execute.

After all that, in the package manager console execute:

Update-Database

If you followed the steps above, there should be no errors.

These are all the changes made in this step.

Step 07: The ApplicationOAuthProvider

Now we're going to borrow from the SPA template that Microsoft provides with VS 2013. In it, there is a file that handles the provisioning of the OAuthAuthorizationServer. You can just copy the code from there wholesale and put it in Providers\ApplicationOAuthProvider.cs. Make sure to change the namespace!

These are all the changes made in this step.

Step 08: Configuring the Authentication Provider

Now, from the ApplicationOAuthProvider class you just copied, remove the ClaimsIdentity cookieIdentity line and the context.Request.Context.Authentication.SignIn(cookiesIdentity) line. These lines are for cookie identity, and we'll only be using bearer authentication in our application.

Start by adding a file called Startup.Auth.cs to the App_Start folder. Visual Studio will recognize the name pattern and the resuling class it generates is appropriately named Startup. Replace that anyways with the following code:

using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Antaramian.SPAAuthenticationExample.Models;
using Antaramian.SPAAuthenticationExample.Providers;
using Owin;

namespace Antaramian.SPAAuthenticationExample
{
    public partial class Startup
    {
        public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
        public static Func<UserManager<IdentityUser>> UserManagerFactory { get; set; }

        static Startup()
        {
            String PublicClientId = "self";
            UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>(new ExampleContext()));
            OAuthOptions = new OAuthAuthorizationServerOptions
            {
                TokenEndpointPath = new PathString("/token"),
                Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
                AccessTokenExpireTimeSpan = TimeSpan.FromHours(24),
                AllowInsecureHttp = true
            };
        }

        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseOAuthBearerTokens(OAuthOptions);
        }
    }
}

As you can see, this is using the ApplicationOAuthProvider from the last step. Here is where we are re-using the partial directive on the Startup class. This class definition will be merged with the class definition in Startup.cs at the root-level of the project at compile-time. We are also creating two static parameters: OAuthOptions, which will contain our application-specific settings for the ApplicationOAuthProvider, and UserManagerFactory which is a helper function for eating a new UserManager session that is properly configured.

The static constructor will be run once per runtime environment of the application, so we handle some default set-up work in this function. First we set the string that the ApplicationOAuthProvider will use to recognize authentications issued by itself. Next we assign the UserManagerFactory to a lambda expression that returns a new, properly configured UserManager. (Note: we are assigning a lambda function to a Func<TResult> delegate, so the functionality will not actually occur until the delegate is called.) We then assign our application specific configuration options to the OAuthOptions parameter which will be passed to the authentication middleware later on.

The TokenEndpointPath defines the location relative to the current application's root where you want the authentication middleware to respond. In this case, the authentication middelware will hanlde all requests to /token. We then specify the actual provider that handles how a user should be checked for authentication; in this case, it's the ApplicationOAuthProvider from the last step. We also set a timeframe for which tokens will be valid once issued. And we finally allow authentication over insecure HTTP transmissions for testing purposes.

The ConfigureAuth() method will be used in the next step to actually add the authentication middleware to the pipeline. It takes the application builder object which we want to add the authentication middleware to and then adds the middleware by calling the UseOAuthBearerTokens() method on the referenced object. It passes in the configuration settings we defined above.

These are all the changes made in this step.

Step 09: Add the Authentication to the Pipeline

Last, but not least, wire up the authentication system to the app by adding the following line before the app.UseWebApi(config) statement in your Startup.cs file:

ConfigureAuth(app);

OK, compile and run time!

You should be able to use an HTTP request tool (my vote is for Postman) to make a call to the API. What you'll need to do is make a POST to http://localhost:{port}/token with x-www-form-urlencoded data of the form "grant_type=password&username=admin&password=password" (or enter these as key->value mappings depending on what your tool supports). For example:

Calling Token authentication endpoint using Postman

Your response should look something like the following:

{
    "access_token": "rC2ligrvhQqQDZlfYHbNvmcLJ2vhrU-R1t61F1WaZVE9ZGiI4QrcQghbdj-MkjeNQsNqEG15QQXUfFb2V7m2TkCDt0HNZANdBM-cY3GUsP7JJcZb_LXo4-3X16nccMUlxKF0ts-_4fgt-2nhfQ5vaEIZQq7dApIL1nRvl--eSSU5rXmR2Kk5cvFcLruOwyMc52CXb2qoYL9vkx4WTyQ8g-J6zxrBRtG37rv0d7xN1QngSt5y-a98vCzA4F1d-UA7TxOVMj7SJXElBlMkAzT-4t0rpOh-LIGKoPEJRwiAV0PSpXtJuVp6x3PptUo8j2rKg-tMAKPzG3M7tzxcMusgDaf5_cKFOQQnOcbdJK2AIK_IYwhX74sOdodIfHgEJDtBDMtZF_yH-Lz6dmVHiUWNV5v6ivTajKvISpazcG8kRps",
    "token_type": "bearer",
    "expires_in": 86399,
    "userName": "admin",
    ".issued": "Sat, 15 Feb 2014 07:47:47 GMT",
    ".expires": "Sun, 16 Feb 2014 07:47:47 GMT"
}

Of course, we have nothing to actually view that requires authentication. But you are definitely authenticated!

These are all the changes made in this step.

Step 10: Creating Some Data For VIPs

To make things interesting, lets create some actual data that we will serve up through the API. We're going to create the following entities:

  • Regions
  • Employees
  • Sales

We'll also set them up to have some relationships between them and insert some default information. You can go ahead and copy in the Employee.cs, Region.cs, and Sale.cs files. The comments in them should be detailed enough to explain what their relation is to each other. Some relations, though, are described in the new ExampleContext class using the fluent API. You'll also have to copy in the new Configuration class which defines some default data. The perform the following operations in the package manager console:

Add-Migration SalesData
Update-Database

These are all the changes made in this step.

Step 11: Serving VIP Data

OK, now we have to serve up that data. Add a new class to the Models folder called RegionSalesSummaryViewModel. This ViewModel provides the limited scope of data we are returning to the user

Also add a new Web API controller to the Controllers folder called RegionsController. The controller defines a single endpoint, /regions/summary, which returns the sales summary data for all regions only for authenticated users. The comments in the code should point you in the right direction as to what is happening, but the one major piece of code to point out is the smallest bit: [Authorize], which is an attribute that requires the user to be considered authenticated in order to proceed.

To demonstrate this, start the debugger. Try an HTTP GET request on http://localhost:{port}/regions/summary. The server should respond with the HTTP status 401 Unauthorized, like in the following picture (emphasis bubble added):

Postman result of an unauthorized HTTP GET request

To actually access the data, you should be able to use the access_token from step 9 (assuming you gave the access_token a reasonable expiration time). If you didn't save the token, you can just regenerate it by calling the HTTP POST method with the username and password on the token endpoint again. Add the "Authorization" header to the GET request with a value of "Bearer tokenData" where tokenData is replaced with the access token. It's important to keep a space between the word Bearer and the rest of the token. In Postman, this looks like the following:

Postman result of an authorized HTTP GET request

You should now get an HTTP 200 OK response with a body like the following:

[
    {
        "Id": 1,
        "Name": "North America",
        "SalesDirector": "Sarah Doe",
        "GrossSales": 10373.26,
        "GrossSalesTarget": 9000
    },
    {
        "Id": 2,
        "Name": "Europe",
        "SalesDirector": "John Q. Public",
        "GrossSales": 4204.16,
        "GrossSalesTarget": 6000
    }
]

You can now authenticate into the server and retrieve data protected by [Authorize] statements. You have also reached the end of the Web API construction section. It's time to move on to the construction of the data view.

These are all the changes made in this step.

The Data View Layer

I am building the data view layer using AngularJS, but I will be using Yeoman as a scaffolding tool. While it is entirely possible to build this solution by creating and linking the files by hand, I find that using Yeoman simply speeds up the process.

About AngularJS, AngularUI Router and the Lot of It

For the purposes of this example, I will be using AngularUI Router, and I will specifically be abusing it so that I do not have routes exposed at the URL level. In fact, I will not have routes at all because AngularUI works on a state-transition basis. It does have a built-in URL-to-state mapping system, but we will only be using that in a limited capacity because the specifications of the original question require only one root path to be exposed and transitions between states not to alter the URL. I would urge anyone considering this to think carefully about the specifications of their application and the implications of this methodology before implementing it. I, personally, do not recommend it.

Thus ends my opinion

Step 12: Generating a New AngularJS Application

So the first thing we will need to do is scaffold a new application. Because I'm using Yeoman (on a Mac no less), that basically looks like this (assuming you have the angular generator installed via npm install -g generate-angular):

mkdir WebUI && cd WebUI
yo angular SPAAuthenticationExample

Yeoman's AngularJS generator in action

This will actually add some more overhead than we need because the Angular generator will provide unit-test scaffolding by default, even though I won't be making use of it for this example. You can browse the generated codebase to see what it provides.

These are all the changes made in this step.

Step 13: Installing AngularUI Router

Now we will need to install one of our major dependencies: AngularUI Router. The current version of AngularUI Router has some compatibility issues with the Bower packaging system, so you'll have to specifically install the specially tagged version if using Bower. Using Bower and Grunt:

bower install angular-ui-router#0.2.8-bowratic-tedium --save
grunt bower-install

The first command installed the dependency to our component cache and declared it as a dependency in bower.json. The second command actually included the necessary script into our index.html. You'll also have to add ui.router to the list of module dependencies for your app in app.js. You can run grunt serve now and see that nothing interesting is actually happening.

These are all the changes made in this step.

Step 14: Setting Up AngularUI Router

Now we have to set-up our app to use AngularUI Router. First we need to change the directive in our primary div from ng-view to ui-view (without assignment). Then we need to pull the header information out of main.html and into the index.html file. Then we can delete the useless About and Contact anchors (with their <li> elements). On the Home link, change the ng-href to ui-sref. The ui-sref directive is used by AngularUI to specify a state to navigate to. This state is defined in the $stateProvider configuration which we will handle next. For now, assign ui-sref a value of "main."

In app.js, delete the preexisting routing information. Your config function body should now be empty. Change the injected providers to $stateProvider and $locationProvider. $stateProvider is the configuration engine for AngularUI Router's state machine. $locationProvider is AngularJS's provider service for manipulating the URL.

Now that we have those dependencies, in our config function body we will first instruct AngularJS to set HTML5 mode to true for the URL:

$locationProvider.html5Mode(true);

This will remove the # symbol from the URL. Then we will implement a state:

$stateProvider
  .state('main',
  {
    url: '/',
    templateUrl: 'views/main.html',
    controller: 'MainCtrl',
  });

Here we are instructing AngularUI Router to add a state main to its state machine. AngularUI Router can assign states to a specific URL, in which case browsing to that URL causes the state machine to enter that state. In this case, we have set the URL to be the root of the application, which means that it is the first state a user will see. This is also the only state we will associate with a URL pursuant to our solution specifications. For this reason, no matter where a user navigates, the URL will not change, and there is no way to access a different state except through JavaScript manipulation.

States are also associated with templates and controllers, just like routes in the AngularJS router. Essentially, the main state will show the user the main view with a scope of MainCtrl.

These are all the changes made in this step.

Step 15: The User Service

The next thing we will do is set-up a service that will provide us with our User authentication framework. We'll start by just creating a service where the user can't login. To scaffold a new service using Yeoman, run the following:

yo angular:service user

For this service, we will define the following in the body:

var userData = {
    isAuthenticated: false,
    username: '',
    bearerToken: '',
    expirationDate: null
};

this.getUserData = function() {
    return userData;
};

Now whatever depends on the service can call User.getUserData() and check the value of the isAuthenticated parameter in the returned object to see whether the user is currently authenticated. To illustrate this, we will also create a new controller called HeaderCtrl that will manage the display of links in the header. With Yeoman:

yo angular:controller header

Pass $scope and User in as services that HeaderCtrl depends on, and then make the body of the controller the following:

$scope.user = User.getUserData();

When the HeaderCtrl is instanced, it will set a reference to the userData object in User as $scope.user. This means that any template that inherits this scope can also call the properties of this object. Now in index.html, set the div element with a class of header to also have an ng-controller directive with a value of "HeaderCtrl" and add the following two navigation links below the Home anchor:

<li ng-hide="user.isAuthenticated"><a ui-sref="login">Login</a></li>
<li ng-show="user.isAuthenticated"><a ui-sref="logout">Logout</a></li>

We have our Login and Logout links! The login link will hide itself when the user is authenticated, and the logout link will show itself when the user is authenticated. You can use this to hide links to states that require authentication. The ng-show and ng-hide directives do have the added side-effect of causing a redraw after Angular finishes compiling the UI, though, so also add the ng-cloak directive to the body so that the body only shows after all compiling has taken place.

Of course, right now the links won't work because those states haven't been defined.

These are all the changes made in this step.

Step 16: Authentication Logic & Logging In

Now, we have to actually write some logic about how to authenticate a user. In this step and the next step, our authentication will handle login and logout functions, but there will be no option to persist the login information across sessions. We will handle that later on.

The authentication service will be using the $http service, so add it as a dependency in the User service.

We will start by adding two more functions to our User service: this.authenticate() and setHttpAuthHeader. this.authenticate() is the function we will expose to actually handle authentication while setHttpAuthHeader() will be a simple helper function that sets the Authorization header in the $http service's default header set. Once this header is set, all uses of the $http service after that will send the Authorization header with the bearer token by default. This is defined as a separate function so that we can also call it from the authentication persistence layer later on.

The this.authenticate() function takes four parameters: a username, a password, a successCallback pointer and an errorCallback pointer. The successCallback pointer and errorCallback pointer will be called based on the result of the HTTP transaction. The errorCallback is expected to take a parameter that is a string which contains the error message.

In the this.authenticate() function, we start by defining a config object which will be handed to the $http service. In the config object, we set the method to POST and assign the URL of the Token endpoint. We also set the Content-Type header to application/x-www-form-urlencoded. This is extremely important because the server will only process the data in this format. The data body is constructed by concatenating the parts of the data string to be sent to the server. Note that there is no encryption being done. On a development server that's fine, but the data can easily be sniffed from packets if the data is not transmitted over a secure connection.

The config object is then handed to the $http service and two promise functions are registered: success and failure. If the HTTP transaction is successful, the success function populates the userData object with the returned data and sets the isAuthenticated flag to true. setHttpAuthHeader() then sets the default Authorization header for the $http service, and the successCallback is called. If the HTTP transaction fails, the error function calls the errorCallback function with an error message. The Web API will return an error_description property for failed authentications, so if this property is not present on the data object, we can assume the serve was never reached. In this, case we return a default error message asking the user to try again later.

In order to actually make this work, we have to have a controller that actually calls the authenticate method. I'm going to create a new controller called LoginCtrl that will handle the actual login form and authentication. Using Yeoman:

yo angular:controller login

To LoginCtrl, I will add $scope, $state, and User as dependencies. I will then define three variables at the scope level so I can interact with them in the view:

  • $scope.username
  • $scope.password
  • $scope.errors

$scope.username and $scope.password are initialized as empty strings while $scope.error is initialized as an empty array. I will also add four helper functions to the controller:

  • disableLoginButton(message)
  • enableLoginButton(message)
  • onSuccessfulLogin()
  • onFailedLogin()

The disableLoginButton() function disables the login button and replaces the button text with a message. By default the message is Attempting login.... enableLoginButton() does the opposite and has a default message of Submit. onSuccessfulLogin() defines what to do after the login is successful and is passed as the callback function to the User.authenticate() method. Right now, it just sets the state to the main state. The onFailedLogin() function defines what to to after the login fails. For now, it will push an error onto the $scope.errors array, unless that error is already on the array, and then re-enable the login button.

The final function, login(), is defined at the scope level so that it can be called by ng-submit from a form. It disables the login button and then passes the necessary parameters to the User.authenticate() method.

I'm also going to generate a corresponding view for the login control that acts as the login form:

yo angular:view login-form

The login-form view is just a form with an ng-submit directive assigned to login(). It also uses an ng-repeat directive to display the errors. The username and password fields are bound to the scoped username and password variable through ng-model.

To wire this up to our application, we need to add a state to the $stateProvider in app.js like so:

.state('login',
{
    templateurl: 'view/login-form.html',
    controller: 'LoginCtrl'
});

Now, when you run the app, you should be able to login! You can tell because the app automatically sends you back to the main page. You should also be able to see the logout link, though it won't do anything yet. To de-authenticate, just reload the app by refreshing the page.

You can also try entering in fake info or shutting down the server to see how errors are presented.

These are all the changes made in this step.

Step 17: Logging Out

Now that we're able to login, let's also make it possible to logout. I'm going to create a new Angular controller called LogoutCtrl using Yeoman:

yo angular:controller logout

I'm not going to create a corresponding logout view, because I really don't feel there's a need for one. However, before I even implement the controller, I need to add some functionality to the User service. In the source file for the User service, I'm going to define a new function called clearUserData() and a new method called removeAuthentication(). clearUserData() is responsible for re-initializing the userData object in the User service while removeAuthentication() is responsible for calling clearUserData() along with any other clean-up work when removing authentication including setting the $http service Authorization header to null. In the future, it will be responsible for deleting persisted data as well.

Now in my LogoutCtrl, I am going to specify that $state and User are LogoutCtrl's dependencies. Then I am going to call User.removeAuthentication() from the body and execute $state.go('main') to send the user back to the home page.

Of course, it does nothing unless you wire it up in app.js:

.state('logout',
{
    controller: 'LogoutCtrl'
});

Since we already defined the link in index.html, everything should work once you reload the app.

These are all the changes made in this step.

Step 18: ui-sref and CSS

As a quick step, note that with some CSS frameworks you will have to indicate that ui-srefs are links too! The Bootstrap framework sets all links to have a pointer cursor, so I added this to my main.scss file for consistency:

[ui-sref] {
    cursor: pointer;
}

These are all the changes made in this step.

Step 19: Getting Protected Data

OK, let's actually use the $http service (via the $resource service, actually) to call our protected API endpoint. Because this task is so simple, I'm not going to bother to create a separate service for fetching the data; I will put it all right into the controller. I'll start a new controller as such:

yo angular:controller salesData

I will declare the dependencies of my SalesDataCtrl as $scope and $resource, and then I will set the body of the function as the following line:

$scope.salesSummaryByRegion = $resource('http://192.168.1.44:42042/regions/summary').query();

And that's it for that file. Now I need to create a new view for it:

yo angular:view sales-data

For the view, I have a table with an ng-repeat on the first row of the table body. Then I just need to hook in the controller and the view to a state on in app.js:

.state('sales' {
    templateUrl: 'views/sales-data.html',
    controller: 'SalesDataCtrl'
})

In order to get to it, we will need to add a link in the header:

<li><a ui-sref='sales'>Sales Data</a></li>

I am pointedly not adding a ng-show or ng-hide directive here so that we can see what happens when an unauthenticated user tries to browse to the page. We will handle redirection for authentication in the next step.

As you can see by running the app, if you try to browse to the page as an unauthenticated user, you'll just see the empty table. There won't be any error (unless you have your JS console open). If you login and then browse to the page, you can see the data just fine.

These are all the changes made in this step.

Step 20: Linking to Authentication-Required States

Let's assume for a second that you want to expose links to pages that require user authentication in order to see. In order to handle this, we will have to exploit the way that AngularUI Router handles exceptions. We will also have to make sure that once we redirect a user to the login form that the user is redirected back to the state they were trying to get to. This requires some fine tuned data management and volleying of information. We will use the User service as repository for information when transitioning between states.

First we will define two new functions in the User service that act as exception constructors: NoAuthenticationException and NextStateUndefinedException. NoAuthenticationException is thrown when the User service is asked whether the user is authenticated and it is unable to provide a valid (that is, not expired) token. NextStateUndefinedException is thrown when the User service is asked to provide the next state to transition to and there is no such state.

Next, we will define a repository for holding the data about the next state. This object, nextState, holds a string called name which is the name of the next state to navigate to and a string called error which is the reason why the transition could not occur.

We also define the helper function isAuthenticationExpired() which checks the expiration date set on the user data against the current time and this.isAuthenticated() which checks the user authentication status. this.isAuthenticated() depends on isAuthenticationExpired(). this.isAuthenticated() will throw a NoAuthenticationException if it cannot find valid authentication data.

For state transitions, we define this.getNextState() which returns the nextState object, unless the name is set to an empty string, in which case it throws a NextStateUndefinedException. The this.setNextState() function takes two parameters and assigns them to the two parameters in the nextState object. this.clearNextState() reinitializes the nextState object to empty details. It is the responsibility of whoever calls this.getNextState() to also call this.clearStateData().

Now, in app.js, we are going to change the definition of the sales state so that unauthenticated users cannot get access:

.state('sales', {
    templateUrl: 'views/sales-data.html',
    controller: 'SalesDataCtrl',
    resolve: {
        user: 'User',
        authenticationRequired: function(user) {
            user.isAuthenticated();
        }
    }
})

AngularUI Router will attempt to resolve the parameters before it transitions to the new state. user.isAuthenticated() will throw an error if the user isn't authenticated and that will stop the state change. We will need to watch the error in the run component of our module:

.run(function($rootScope, $state, User) {
    $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error){
        if (error.name === 'AuthenticationRequired') {
            User.setNextState(toState.name, 'You must login to access this page');
            $state.go('login', {}, {reload: true});
        }
    });
})

Essentially what we are doing here is registering a listener on the $stateChangeError event. When it fires, it passes all the current data into the parameters defined in our anonymous function along with the error that caused it to occur. We know that the error that stops state changes because of user authentication will have a parameter with a name of AuthenticationRequired. We then capture the target state information and pass it to User.setNextState() with an error message of "You must login to access this page." We then tell AngularUI Router to transition to the login state. We also set reload to true so that if someone is trying to access a protected page from the login page, the login page will display the error.

Now, we actually have to handle this in LoginCtrl.

The first thing we do is set a local variable called nextState which is initialized as null. In the main body, we then have a try/catch statement which interacts with User.getNextState(). It tries to get the next state that we should transition to, if any. If there is no next state, it ensures that nextState is null which indicates that this is a normal login request.

Next, we check whether the nextState is not null. If this evaluates to true, we have to do some fancy variable juggling to create copies of the original strings:

if(nextState !== null) {
    var nameBuffer = nextState.name + '';
    var errorBuffer = nextState.error + '';
    User.clearNextState();
    nextState = {
        name: nameBuffer,
        error: errorBuffer
    };
    // some other work
}

In a nutshell, because User.getNextState() is returning an object by reference, when we call User.clearStateData(), it will just zero out the data we need to transition. We have to call clearStateData at some point though, as a general housekeeping rule, otherwise the next time the user uses the authentication screen, they could be taken to an unexpected state.

Essentially we concatenate both strings with a blank string (which returns a new string object) and store them in buffer variables until after clearStateData is called. Then we recreate the nextState object using those buffer variables. It then checks whether there is any error being passed in. If there is a string based error that is not empty and not already in the errors array, it pushes it onto the array. Otherwise, it assumes that, since there is nextState data, someone tried to access a protected page and it pushes a default "You must be logged in to access this page" message.

onSuccessfulLogin() must also be redefined. Instead of redirecting the user to the main state, it now checks whether a nextState is available, and if so, sends the user there instead of to the main state.

After making these changes, you should be able to click on the "Sales Data" link from the header bar and you will be presented with the login screen with errors displayed. If you login, you will be sent back to the "Sales Data" page where you can now see the data.

These are all the changes made in this step.

Step 21: Authentication Persistence

Last but not least, we need a way for a user to be able to save their authentication between browser sessions. This type of "remember me" functionality will be done using a cookie, and it doesn't require us to define any more services or controllers or views. First let's go into the User service and add the $cookieStore dependency.

We will define two new exceptions: AuthenticationExpiredException, which will be thrown when the system retrieves a cookie but the authentication has already expired and is therefore no longer valid, and AuthenticationRetrievalException which will be thrown when there is no stored cookie data. We also define the functions saveData() and removeData(). saveData() serializes the authData object as a cookie while removeData() deletes any serialized data.

The retrieveSavedData() function is responsible for deserializing cookie data. If no such data exists, it will throw an AuthenticationRetrievalException. If it successfully retrieves data, it checks to see if the data is still valid using isAuthenticationExpired(). If the data is not valid, it throws an AuthenticationExpiredException. If the data is valid, it sets the userData object to the deserialized data and sets the HTTP header appropriately using setHttpAuthHeader().

In order for this to work, though, we must call these functions from an exposed function. this.isAuthenticated() works perfectly because its duty is to check whether a user is authenticated. Now it will also check for saved data. Like before, this.isAuthenticated() will see if there is any current userData and if it is expired. If the current data is valid, it will simply return true to indicate that the user is logged in. If not, it will attempt to retrieve any saved data using retrieveSavedData(). If retrieveSavedData() throws an error, this.isAuthenticated() will throw an error as well. Otherwise, it will return true, indicating that the retrieval was successful and the user is now logged in. Note: This function never returns false, only true or an error. This is important for the way that we set up authentication re-routing in the previous step.

We also need to update the this.removeAuthentication() function so that it removes stored auth cookies on logout by calling removeData().

Now, this.authenticate() must be updated so that it knows whether to persist the authentication information. It will now take a persistData parameter which it evaluates in the event of a successful HTTP transaction. If the parameter evaluates as true, the authentication data is serialized to cookie form so that it can be picked up by later instances of the app. Regardless, this.authenticate() also calls removeAuthentication() at the start of its execution since any call to authenticate indicates that the user is not currently authenticated and any old data should be removed.

In our LoginCtrl, we need to set a new scoped variable called $scope.persist which defaults to false. We also insert it into our call of User.authenticate(). We also need to define the variable as a checkbo checkbox on the login form that is bound to the persist variable.

The last thing we need to do is change our module run in app.js to call User.isAuthenticated() as such:

.run(function($rootScope, $state, User) {
    try {
        User.isAuthenticated();
    } catch(e) {
        //do nothing with this error
    }
    // $stateChangeError watch here...kept out for brevity
})

When you login now, you will have the option to persist your session! You can test this by logging in with the "Remember Me" checkbox activated, then closing the browser, and the opening the app back up. You should see the logout link in the heaer instead of the login link when the app starts. This means that the app is properly fetching the authentication data from the stored cookie!

These are all the changes made in this step.

Congratulations!

This concludes the step-by-step example for authenticating a user via ASP.NET Web API in an AngularJS app using AngularUI Router without mapped routes for states.