AngularJS SPA Authentication with ASP.NET Identity 1.x
The following is a tutorial that I wrote in February of 2014 on how to provide a backend for an AngularJS SPA using ASP.NET Web API and ASP.NET Identity. I am posting it here for future reference, though the original is still hosted on BitBucket along with the source files referenced in this tutorial. As of this writing, ASP.NET Identity 1.x has been superseded by ASP.NET Identity 2.x, but I believe what is discussed in this tutorial is still relevant to the overall theory of how to accomplish the task.
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.
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 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 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:
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):
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:
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
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-sref
s 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 callUser.clearStateData()
, it will just zero out the data we need to transition. We have to callclearStateData
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.