Windows Authentication with .NET Core API and Angular on IIS
Ho-ly-shit…! Getting this stuff to work properly has been the biggest waste of my time over the past month like you cannot believe!
I recently had the challenge of configuring Windows Authentication on a .NET Core API that would be consumed by an Angular 5 UI, contained in another .NET Core project, both of which were deployed to IIS 10 on the same server. This process was painful to say the least…
Through this deployment and configuration, I learnt a lot about setting up this type of authentication, but also felt that there wasn’t a lot of useful info regarding the complete process, so here’s a complete how-to, from me, to you.
Prerequisites
- Have a basic understanding of Windows Authentication
- A Visual Studio .NET Core Angular UI project
- A Visual Studio .NET Core (Web) API
- A Windows application server (ideally OS 2016) where you can deploy your services to
Setting up IIS
Believe it or not, you can actually configure some components of IIS without the components actually being installed… And then unexpectedly sit there for hours wondering why your shit isn’t working.
Lesson learnt: install all the components first!
We are obviously going to need IIS to handle Windows Authentication, so we obviously shouldn’t install that feature… (install it).
While we are in the Security section, let’s also install URL Authorisation. This feature will give us granular control over the HTTP request headers allowed per Authentication type of each of our sites. We will have to use this to handle the HTTP preflight requests, which we will cover later on below.
During the configuration process, you will undoubtedly have issues that we will need to debug. IIS provides an easish solution for debugging with the Tracing feature. More on this in the Nugget section below, but for now, let’s add it.
Some other features that you may need depending on your method of deployment, is IIS Management Scripts and Tools, as well as IIS Management Service. These will become necessary/useful if you make use of the Publish option on your Visual Studio projects.
That’s it for IIS features to install. Complete the installation wizard.
Configuring IIS
The next step that we have to do is configure IIS. The first part of this is creating the Application Pool and Site that we will deploy our API and UI to.
Creating the App Pool
This section is pretty straight forward, but first, what is an app(lication) pool?
An Application Pool can contain one or more applications and allows us to configure a level of isolation between different Web applications… Because each application pool runs in its own worker process, errors in one application pool will not affect the applications running in other application pools. Deploying applications in application pools is a primary advantage of running IIS 6.0 in worker process isolation mode because you can customize the application pools to achieve the degree of application isolation that you need.
When you configure application pools for optimum availability, you also should consider how to configure application pools for application security. For example, you might need to create separate application pools for applications that require a high level of security, while allowing applications that require a lower level of security to share the same application pool.
My personal preference is to isolate applications in their own pools. We will apply this to our current situation and deploy the API and UI to their own pools.
Open IIS Manager which we installed in the beginning. In the Connections pane, right click Application Pools and select Add Application Pool.
Insert the name of your App Pool. We will keep it simple and name our pools UI and API. We will keep the other settings as default.
You should now have two application pools.
It will make our lives easier later on if we run our application pools under domain credentials as we will be able to apply granular approach to the access that the site and pool possess. Let’s configure the account that is running the app pool.
Right click on the app pool and select Advanced Settings.
Under the Process Model group, you will find the Identity option. Select the ellipses button to configure the identity/account running the pool.
This will open the below screen. Select the Set button, and type in the domain credentials that you will be using to run the app pool
The Identity section of the App Pool details should now reflect your domain credentials
At this point I suggest waiting for 10 seconds and pressing F5 and double check the status of your App Pool. If it is in a stopped state, you will need to open Event Viewer and have a look at the Application section. IIS will log failures there. More likely than not, your domain credentials that are running the app pool do not have Log on as a batch job
, and/or Log on as a service
.
You can fix this by opening up Local Security Policy, navigating to Local Policies > User Rights Assignment, and adding the credentials running the pool as a member of the relevant policies.
Its likely that your organisation will have locked-down the editing of these groups. If they have, contact someone in the Windows Server Infrastructure team, or contact your Active Directory team to assist you with editing these policies.
That should be it for creating the app pools. The next section will involve the consumption of the pools for the relevant Sites.
Creating Sites
Next we need to create a Site that will hold our application binaries, but first, what are Sites in IIS:
A site is a container for applications and virtual directories, and you can access it through one or more unique bindings.
Let’s create a new site each for our UI and API. In the Connections pane, right click Sites, and select Add Website:
This will open the below menu. In my experience, I do not place anything on the C:\ drive unless its directly related to the OS. Everything else I would prefer to place on an additional drive, which other support teams are less likely to mess around on. I have heard contradicting schools of thought though so use a location that works best for you.
My preferred port range is also 808# for HTTP and 4438# for HTTPS. This way it is easier to keep track of which ports are for which sites and which protocol.
Our API is not front facing so ideally we would like to reserve the default ports of 80 and 443 for our UI, making the URL in the client’s browser cleaner and more user friendly (the default ports are hidden).
Now depending on how you will handle your site access and its corresponding directory access, you will need to configure your site appropriately. You will probably be applying application logic with the API as well as UI to limit users by AD group membership, while limiting IIS Site directory access by the application pool identity and some other IIS user or group (this is the more simpler and straight for option). This was one of the more challenging aspects for me as it requires you to identify the required groups based on your configs and then the required permissions per group or user.
Lo and behold, the gold standard for ACLs (account control lists). It took me weeks to find and understand that link… Take the time to understand this as it will have a significant impact, especially for the 401.3 errors that you will likely experience at some point. Notice the discrepancy between 401.2 and 401.3. There was a point where I thought the ACLs were involved in the 401.2 errors which is not the case.
In summary, grant the following site directory ACLs:
- Grant [IUSR]
Read & Execute
,List Folder Contents
, andRead
access. This is the identity that will map to the Anonymous Authentication domain credentials. Changing the domain credentials does not require a change in the ACLs as this user maps to it. - Grant the Worker Process Identity [IIS AppPool\<AppPoolName>]
Read & Execute
,List Folder Contents
, andRead
. If you will be uploading content, then it is suggested that you should isolate the folder permissions for the upload directory to only have theWrite
and exclude theExecute
permissions. This is the identity that will map to the Windows Authentication domain credentials when theuseAppPoolCredentials
config setting is set to True. Changing the credentials does not require a change in the ACLs as this user maps to it.
Don’t worry too much about applying the useAppPoolCredentials setting above at the moment. We will take you through it later on in this article. The point was to show you the relationship between the ACLs and the App Pool Identity.
Note: the official suggested method for running Windows Authentication is to “lock down all resources to a group containing all authenticated users”. The delay associated with getting the AD groups ironed outed makes this method impractical though, thus the above suggested Worked Process Identity permissions.
Make sure that the ACLs you select are inline with you organisation’s Logical Access Management and Security policies where the site will be deployed.
Configuring the Authentication
The creation of the sites and their configurations overlap slightly as each deployment through Visual Studio’s Publish will overwrite the web.config
file which contains some, but not all, of the configuration settings being applied to your site. This is because of the .NET Core aspect of the project, which differs from .NET Framework whereby the site configs are separate from the your project to allow the target environment diversity and compatibility.
After each clean deployment, you should go over each of the configs and confirm that what has already been applied is still there. This replacement per deploy can cause a lot of frustration and prompts configuration automation via some kind of tool. A basic option is PowerShell, however some aspects of an IIS site’s configs are by default locked down, preventing external tools from changing them. Play around and find a tool that works for you.
In any case, let’s get down to it. On IIS Manager, on the main pane, under the IIS section, double click the Authentication menu option. This will open up the below screen. Enable the Windows, ASP.NET Impersonation, and Anonymous Authentication only. All other forms of authentication should be disabled (i.e. Basic and Forms amongst others). Note that we installed the Windows Authentication as a feature on IIS in the beginning.
I found that in some OS deployments, the ASP.NET Impersonation option was missing from IIS. To fix this, in the Windows Features dialog, make sure that the features ASP.NET and .NET Extensibility features are installed.
In our situation, both the API and UI make use of Windows Authentication. Unless we want the front end user to manually authenticate twice, once for the UI and once for the API (in that order due to the HTTP request sequence), then you will need to configure IIS to carry over the credentials from the first authentication through to the rest of the calls to other sources, specifically the API. We can do this by making use of ASP.NET Impersonation authentication method in IIS, and is why we have enabled it. By default, it is set to impersonate the Authenticated user. In our case our API applies security based on the user that is making the requests to it, which means the default config for the ASP.NET Impersonation will work correctly for our situation
While we are here, edit Anonymous Authentication to make use of the application pool identity. This is a better security practice and makes our ACLs easier.
Similarly for Windows Authentication, we will need to make use of the Configuration Editor for your site:
Navigate to the windowsAuthentication
section and change the value for useAppPoolCredentials
to True
.
As for why we have enabled Anonymous authentication, you might be asking yourself: “but we want Windows authentication, so we shouldn’t be enabling anonymous authentication?”
This is because we need to cater for preflight request. Down the tunnel we go…
Preflight Requests with IIS
Before we can even get to preflight requests, we need to understand what CORS is:
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin. A web application makes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, and port) than its own origin.
We need to acknowledge the significance of HTTP headers. They run the HTTP underworld… As for CORS, you will run into them eventually on your browser’s console, especially if you are using Chrome (its very picky but that’s good for security). Now for preflight requests:
A CORS preflight request is a CORS request that checks to see if the CORS protocol is understood.
It is an
OPTIONS
request, using three HTTP request headers:Access-Control-Request-Method
,Access-Control-Request-Headers
, and theOrigin
header.A preflight request is automatically issued by a browser, when needed. In normal cases, front-end developers don’t need to craft such requests themselves.
And a bit more, HTTP headers can be categorized based on their context:
General header: Headers applying to both requests and responses but with no relation to the data eventually transmitted in the body.
Request header: Headers containing more information about the resource to be fetched or about the client itself.
Response header: Headers with additional information about the response, like its location or about the server itself (name and version etc.).
Entity header: Headers containing more information about the body of the entity, like its content length or its MIME-type.
And finally, some of the more common Request headers that we will be working with:
GET: The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should have no other effect. (This is also true of some other HTTP methods.)[1] The W3C has published guidance principles on this distinction, saying, “Web application design should be informed by the above principles, but also by the relevant limitations.”[17] See safe methods below.
HEAD: The HEAD method asks for a response identical to that of a GET request, but without the response body. This is useful for retrieving meta-information written in response headers, without having to transport the entire content.
POST: The POST method requests that the server accept the entity enclosed in the request as a new subordinate of the web resource identified by the URI. The data POSTed might be, for example, an annotation for existing resources; a message for a bulletin board, newsgroup, mailing list, or comment thread; a block of data that is the result of submitting a web form to a data-handling process; or an item to add to a database.[18]
PUT: The PUT method requests that the enclosed entity be stored under the supplied URI. If the URI refers to an already existing resource, it is modified; if the URI does not point to an existing resource, then the server can create the resource with that URI.[19]
DELETE: The DELETE method deletes the specified resource.
OPTIONS: The OPTIONS method returns the HTTP methods that the server supports for the specified URL. This can be used to check the functionality of a web server by requesting ‘*’ instead of a specific resource.
Getting back to our scenario, basically we need to ensure that our solution can adequately handle preflight requests. We can do this by configuring IIS to force specific types of authentication based on the request headers that are being sent through (applies to both the UI and API).
An FYI, I noticed that at one stage, preflight request were being sent through for GET methods, which isn’t supposed to be the case, but luckily our solution will not discriminate against them.
Basically we want to allow the OPTIONS method to be allowed to go through with Anonymous authentication, and every other request header /method to be challenged with Windows Authentication. The reason for this is that HTTP requests that make use of the OPTIONS header, by default do not carry credentials/authentication tokens with them. This would prevent the authentication against Windows and throw errors in your browser/application.
If we were making use of .NET Framework, we could have probably made use of a different solution with AuthenticationSchemeSelectorDelegate
, however are are instead making use of .NET Core (a pain in the ass if you are still going to be deploying to a Windows environment…).
Getting (:D that was not planned) to the solution for .NET Core, we can make use of a feature in IIS called URL Authorization. This will allow us to govern which authentication policy will be applied per HTTP request header.
An FYI, you can find the configuration for these rules at Configuration Editor path: system.webServer/security/authorization
.
You might at this point think that we are done, but nope, you would indeed be wrong yet again. The HTTP header management we did above was one side of the coin. The other side is configuring the site to handle CORS in general. You can do this by adding the below configs to your site (web.config):
<system.webServer>
<httpProtocol>
<customHeaders>
<!-- <add name="Access-Control-Allow-Origin" value="*" /> -->
<add name="Access-Control-Allow-Methods" value="GET,PUT,POST,DELETE,OPTIONS,HEAD" />
<add name="Access-Control-Allow-Headers" value="Content-Type, Accept"/>
</customHeaders>
</httpProtocol>
</system.webServer>
I found that when enabling Anonymous Authentication alongside Windows Authentication in IIS, the Origin aspect of CORS was handled for you, thus the section is commented out above. For a list of some additional values for the Headers section, read up here.
If you found that the URL Authorisation rules are not working at all, double check that the feature is actually installed… And maybe throw in an iisreset
in CMD.
Otherwise we are finally done with configuring IIS. We now need to create the SPNs.
SPNs for Your IIS Sites
Ok, what are SPNs:
A service principal name (SPN) is a unique identifier of a service instance. SPNs are used by Kerberos authentication to associate a service instance with a service logon account. This allows a client application to request that the service authenticate an account even if the client does not have the account name.
Why do we need them? To get negotiate to work properly and to assist with the double hop issue with Windows Authentication.
When creating SPNs for HTTP sites, you make use of the HTTP Service Class. “ The HTTP service class differs from the HTTP protocol. Both the HTTP protocol and the HTTPS protocol use the HTTP service class.”
There are plenty of tutorials out there for creating SPNs so I won’t be covering that. Just remember to create them…
Below are some useful tips and tricks I learnt along the way.
Some Additional (Chicken) Nuggets
URL Rewrite for HTTPS
If your solution is for a production environment, then you probably need to handle non-SSL connections. A more user-friendly approach is to redirect the user to an SSL connection/binding on IIS. You can do this by making use of URL Rewrite. We will cover how to for this below.
The URL Rewrite feature is external to IIS, so you will need to find the installer and sort it out. You can download the extension from IIS.net or an older version directly from Microsoft.
Once installed, the feature will be visible in the configuration
You will probably eventually run into the problem of having to redirect to a URL that has a port not set to either 80 or 443. This creates a problem as these ports are automatically handled by the browser, for all other ports, you will need to handle them in the rules you create for your Rewrites.
For rules to an HTTPS URL not using the standard 443 port, you can do the following in your web.config (/system.webServer/rewrite/rules)
:
<rule name="HTTP to HTTPS on different SSL Port" enabled="true" stopProcessing="true">
<match url="(.*)" ignoreCase="true" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="true">
<add input="{HTTPS}" pattern="off" />
<add input="{HTTP_HOST}" pattern="([^/:]*?):[^/]*?" />
</conditions>
<action type="Permanent" url="https://{C:1}:1443/{R:0}" appendQueryString="false" />
</rule>
Similarly for a redirect to an HTTPs binding on 443:
<rule name="HTTP to HTTPS on different SSL Port" enabled="true" stopProcessing="true">
<match url="(.*)" ignoreCase="true" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="true">
<add input="{HTTPS}" pattern="off" />
</conditions>
<action type="Permanent" url="https://{HTTP_HOST}{REQUEST_URI}" appendQueryString="false" />
</rule>
You may want to play around with the Redirect Type
of Permanent
vs Found
, especially during testing, as the browser may remember Permanent
redirects causing a bit of frustration if you port decisions are not yet set in stone.
Tracing Authentication Failures On IIS
IIS has a feature call Tracing, which we installed earlier, that we can make use of to debug authentication on IIS sites. We first need to enable tracing, and then create rules that will listen for and capture matching events.
Microsoft has a tutorial for this which is great. I would however suggest changing the Status Code
to 200–500
and the Areas
to Security
as well as Authentication
With this rule in place, you should be capturing events that will assist you with vetting the authentication.
Microsoft was kind enough to provide us with some styling for the logs. If you open the dumps in Internet Explorer, the ‘unsafe’ styling will be loaded and provide you with a readable view
Selecting the second tab at the top labelled ‘Request Details’ will provide you with the authentication part of the session:
system.web vs system.webServer
At some point when Googling for solutions to some of the problems above, you probably came across one that made use of the system.web
section of the Config Editor.
What should you?
That shits from IIS 6. We don’t use that any more. If the solution is telling you to put some shit in there, spare some time to down vote it, then Google some more and find a better (and probably a more recent) solution.
Edit 1 (4 April 2019):
An extra nugget that I found only after creating this post, was that I was still struggling with Chrome prompting for credentials for both the API and UI. At first I thought that this was because of some random incorrect SPNs and/or delegation settings, but after checking these multiple times I gave up. It was only once I starting using IE (yes…) did I realise that the issue disappeared... With a bit of Googling I found out that there were GPOs (Group Policy Objects) managing the Trusted and Local Intranet sites and seems to be the route of the issue. For some reason, Chrome does not treat the Local intranet sites that same as IE. In order to resolve this issue, please follow this stack overflow thread.
—
If you were hoping to see something in here that isn’t, let me know in the comment section below and I may just add it… Anyways, hope you enjoyed the how-to. Live long and prosper…
Edit 2 (26 June 2020):
NetCore is sexy. Use it when possible taking into consideration the limitations when integrating with NetFramework. Use NetStandard when needed.