How to implement Windows Authentication in an AngularJS application with a stand-alone Web API

How hard can this be, really? Sounds simple enough and once you figure it out, it really is. But as it took me some research on various small issues along the road, I thought, why not bring all the pieces of the puzzle together. And so here we are.

In this post, I am going to build a small sample application from the ground up. I will focus on the key points of getting everything set up correctly.

Let’s give this post some clear boundaries:

  • Angular JS web project
  • Web API project
  • Windows Authentication

Creating the solution

First, we’ll need a Web API project for the backend. We only want the Web API part, so we pick the Empty template and check Web API. As you can see, we cannot change the authentication at this point. We’ll get to that later on.

howto-ng-winauth-01

Next, we’ll create the AngularJS application. I’m using a very basic company template here to avoid having to setup too much repetitive stuff. Any web project where you add Angular into will do.

howto-ng-winauth-02

The solution will look something like this:

howto-ng-winauth-03

Configure IIS Express

At this point we are ready to configure our IIS Express to use Windows Authentication. At the moment I’m using Visual Studio 2015, which means I have a (hidden) .vs-folder in my solution folder.

howto-ng-winauth-04

Drilling down into the folder we come across a config folder which contains an application.config file. We can alter this file for solely the solution which we are working on.

howto-ng-winauth-05

Let’s set the authentication inside this application.config file as follows:

<authentication>
	<anonymousAuthentication enabled="false" userName="" />
	<basicAuthentication enabled="false" />
	<clientCertificateMappingAuthentication enabled="false" />
	<digestAuthentication enabled="false" />

	<iisClientCertificateMappingAuthentication enabled="false">
	</iisClientCertificateMappingAuthentication>

	<windowsAuthentication enabled="true">
		<providers>
			<add value="Negotiate" />
			<add value="NTLM" />
		</providers>
	</windowsAuthentication>
</authentication>

Setting up our Web API

For the interest of this post, I created 2 Controllers in the Web API:

  • WinAuthController
    This can be used to retrieve some additional information for the logged in user using some other resource such as an SQL database.
  • DataController
    Our controller that will be handling our very important data.

Add both controllers to the Web API project as empty Web API 2 controllers.

howto-ng-winauth-07

I also added a model to my Web API project ‘PostData’ which will hold our data.
We now have this:

howto-ng-winauth-08

The implementation:

  • PostData

    public class PostData
    {
    	public int Id { get; set; }
    	public string Name { get; set; }
    	public bool IsTrue { get; set; }
    	public DateTime CreatedOn { get; set; }
    
    	public override string ToString()
    	{
    		return $"PostData with [Id: {this.Id.ToString()}] - [Name: {this.Name}] - [IsTrue: {this.IsTrue.ToString()}] - [CreatedOn: {this.CreatedOn.ToString("dd/MM/yyy")}].";
    	}
    }
    
  • WinAuthController
    [Authorize]
    [RoutePrefix("auth")]
    public class WinAuthController : ApiController
    {
    	[HttpGet]
    	[Route("getuser")]
    	public IHttpActionResult GetUser()
    	{
    		Debug.Write($"AuthenticationType: {User.Identity.AuthenticationType}");
    		Debug.Write($"IsAuthenticated: {User.Identity.IsAuthenticated}");
    		Debug.Write($"Name: {User.Identity.Name}");
    
    		if (User.Identity.IsAuthenticated)
    		{
    			//return Ok($"Authenticated: {User.Identity.Name}");
    			return Ok("Authenticated: yourdomain\\yourUserName");
    		}
    		else
    		{
    			return BadRequest("Not authenticated");
    		}
    	}
    }
    
  • DataController
    [Authorize]
    [RoutePrefix("data")]
    public class DataController : ApiController
    {
    	[HttpPost]
    	[Route("save")]
    	public IHttpActionResult Save(PostData data)
    	{
    		return Ok(data.ToString());
    	}
    }
    

As we are implementing the Web API separate from the Angular application, they are both running on different ports in IIS Express. We’ll have to setup CORS (Cross Origin-Requests).

Add the package to the Web API project through NuGet:

howto-ng-winauth-12

Let’s configure the Web API for CORS.

var cors = new EnableCorsAttribute("*", "*", "*");

Setting it up like this opens up the Web API for every origin, header and method. But this doesn’t suit our needs. Because we want to use Windows Authentication, we have to configure CORS in such a way that it knows we are passing credentials. This means changing the EnableCorsAttribute to this:

var cors = new EnableCorsAttribute("http://localhost:30033", "*", "*") { SupportsCredentials = true };

Setting SupportsCredentials to true tells our Web API that credentials can be passed in the request. It also requires us to change the origins configuration to actually say which origin(s) are allowed to call our Web API. You can’t use SupportsCredentials as true along with * for the origins.

We are going to call the Web API from our Angular application, so I’m going to set up the Web API to return json instead of XML.

This is done like so:

config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));

All this configuration is done in the WebApiConfig file (in the App_Start) folder of the project.

The complete file now looks like this.

Just one change in the web.config file and we are done in the Web API. Set the authentication mode to Windows.

<authentication mode="Windows"/>

Setting up our Angular application

I’m using Angular 1.5.7. without using the component router. Also, I started using Typescript for Angular projects a while back now. If you don’t use Typescript (yet), I advise you to reconsider. I won’t go into the details about why you should use Typescript (in my opinion), but do give it a shot!

So the template I used gave me a basic functional Angular application with a Home and a Help page. Both of which can be accessed through a navigation bar.

We’ll start by adding 2 services to the application. One service per controller in our WebAPI. So we have this:

  • authenticationService
    getUser(): ng.IPromise<string> {
    	const self = this;
    	self.logSvc.log('Calling getUser');
    
    	let defer = Q.defer();
    	let serviceUrl: string = `${C.Configuration.AppConfig.serviceBaseUrl}auth/getuser`;
    
    	self.$http.get(serviceUrl)
    		.success((data, status, headers, config) => {
    			defer.resolve(data as string);
    		})
    		.error((data, status, headers, config) => {
    			self.logSvc.log(data);
    			defer.reject(new M.RejectMessage({ Message: data }));
    		});
    
    	return defer.promise;
    }
    
  • dataService
    save(data: M.IPostDataModel): ng.IPromise<string> {
    	const self = this;
    	self.logSvc.log('Calling getUser');
    
    	let defer = Q.defer();
    	let serviceUrl: string = `${C.Configuration.AppConfig.serviceBaseUrl}data/save`;
    
    	self.$http.post(serviceUrl, data)
    		.success((data, status, headers, config) => {
    			defer.resolve(data as string);
    		})
    		.error((data, status, headers, config) => {
    			self.logSvc.log(data);
    			defer.reject(new M.RejectMessage({ Message: data }));
    		});
    
    	return defer.promise;
    }
    

As in the Web API, each of the services has 1 method.

Next we’ll add a model to our app which will be the client side representation of our server side PostData object.

module DeBiese.WinAuth.NG.Models {

    export interface IPostDataModel {
        Id: number;
        Name: string;
        IsTrue: boolean;
        CreatedOn: Date;
    }

    export class PostDataModel implements IPostDataModel {

        public Id: number;
        public Name: string;
        public IsTrue: boolean;
        public CreatedOn: Date;

        constructor(obj?: IPostDataModel) {
            if (obj != null) {
                this.Id = obj.Id;
                this.Name = obj.Name;
                this.IsTrue = obj.IsTrue;
                this.CreatedOn = obj.CreatedOn;
            }
        }
    }
}

The home page will be used for our test implementation, so we inject our 2 services into the homeController and add 2 methods for calling those services.

testAuthentication(): void {
	const self = this;

	self.authSvc.getUser()
		.then((rslt) =&gt; {
			self.toastSvc.toastSuccess(rslt);
		})
		.catch((err) =&gt; {
			self.logSvc.log(err);
			self.toastSvc.toastError('An unexpected error occurred.');
		});
}

testPostData(): void {
	const self = this;

	self.dataSvc.save(new M.PostDataModel({ Id: 1, Name: 'DeBiese', IsTrue: false, CreatedOn: new Date() }))
		.then((rslt) => {
			self.toastSvc.toastSuccess(rslt);
		})
		.catch((err) => {
			self.logSvc.log(err);
			self.toastSvc.toastError('An unexpected error occurred.');
		});
}

We’ll add 2 buttons to our homeView which will invoke those methods.

<button type="button" class="btn btn-default" ng-click="vm.testAuthentication()">Authentication</button>
<button type="button" class="btn btn-primary" ng-click="vm.testPostData()">Post Data</button>

Like we did with the Web API, we’ll have to tell Angular to pass credentials when doing http requests. We do this by injecting the $httpProvider into our configuration class for the angular app and setting withCredentials to true.

$httpProvider.defaults.withCredentials = true;

And we are done (for now)!

Testing the application

Let’s fire up both the Web API and the Angular application.

howto-ng-winauth-23

Our two buttons are in place. So let’s click the Authentication button.
The result on the page is as follows:

howto-ng-winauth-24

Good, a http get is working. Now for the post. Let’s click Post Data.
And the result:

howto-ng-winauth-25

Woops.

Opening up dev tools, we see this error message:

howto-ng-winauth-26

What’s happening here? It’s saying that the response to the preflight request doesn’t pass the access control check.

For those of you that are not familiar with a preflight request, you can find some more information here.

The reason our little application is sending a preflight request, is that we are sending data as application/json which is not one of the possibilities for using a http post without sending a preflight request.

If you are like me and you get that error message, you will start googling for that preflight request and why the response does not pass the check. Which would throw you off the right path right away. That’s because further on in the error message, you’ll find the actual issue. Which is: The response had HTTP status code 401.

So our preflight request couldn’t get authenticated. This is perfectly normal because a preflight request DOES NOT send authentication information and we configured our Web API to not allow anonymous authentication.

 

While setting up the Web API, we decorated our controllers with the Authorize attribute thus ensuring that only authorized requests can pass through. This means that we can once again modify our application.config file to allow anonymousAuthentication.

<anonymousAuthentication enabled="true" userName="" />

In addition, you should also modify your web.config file in the Web API project to allow anonymous users. This is because otherwise visual studio adds a section to the application.config file to overwrite the anonymousAuthentication setting we just set to true.

<authorization>
  <allow users="?"/>
</authorization>

This setting will ensure that preflight requests get handled correctly by our Web API without responding with an HTTP 401.

And there you have it, pushing the Post Data button again will return the actual expected result from the controller.

howto-ng-winauth-27

An interesting side note, which I find worth mentioning, is that Internet Explorer (11) does not respond with the preflight request error when anonymousAuthentication is disabled.

Summary

  • application.config
    <authentication>
    	<anonymousAuthentication enabled="true" userName="" />
    	<basicAuthentication enabled="false" />
    	<clientCertificateMappingAuthentication enabled="false" />
    	<digestAuthentication enabled="false" />
    
    	<iisClientCertificateMappingAuthentication enabled="false">
    	</iisClientCertificateMappingAuthentication>
    
    	<windowsAuthentication enabled="true">
    		<providers>
    			<add value="Negotiate" />
    			<add value="NTLM" />
    		</providers>
    	</windowsAuthentication>
    </authentication>
    
  • Web API
    • Add CORS
    • Configure CORS
      var cors = new EnableCorsAttribute("http://localhost:30033", "*", "*") { SupportsCredentials = true };
      
    • Configure for json instead of XML
      config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
      
    • Web.config settings
      <authentication mode="Windows"/>
      <authorization>
        <allow users="?"/>
      </authorization>
      
  • Angular app
    • Configure $httpProvider (withCredentials)
      $httpProvider.defaults.withCredentials = true;
      

Closing word

I hope this post provides you some insight into the matter. Getting this to work correctly took me a while. Looking back, it was mostly due to the preflight request error which caught my attention and threw me off. I was too focused on the start of the message that I at first missed the HTTP 401 response.

The full code base of this little test project can be found on GitHub.

Feel free to comment down below!

 

Ruben Biesemans

Ruben Biesemans

Analyst Developer @ Spikes

Advertisements

2 responses to “How to implement Windows Authentication in an AngularJS application with a stand-alone Web API

  1. Pingback: How to – Authorization in AngularJS application with Windows Authentication | Spikes Apps·

  2. Pingback: Week 8: Algemene tweaks en authenticatie | Stageblog ArcelorMittal·

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s