Protecting a .NET Core 2.0 SPA with ADFS


Today's identity-related pop quiz: How do you secure a SinglePageApp (SPA) with a .NET Core back-end using ADFS?

If you said "there's probably an official sample for that over at docs.microsoft.com" you'd not be entirely right. Oh, there's samples, but if you implied "working sample" I dare you to come up with it 🙂 (Well, of course I don´t know when you are reading this. Maybe there´s a super-duper sample that came out five minutes after I hit the publish button. Or maybe I didn´t look in the right places, but official docs should not be hard to locate.)

So, how about the templates in Visual Studio - they come with boilerplate code for a lot of things, right? Yes, they do. But notice how the "Change Authentication" button is greyed out if you choose a JS Framework. (Whether you choose Angular or React it's still disabled.)

If you bear with me we'll take a tour through some code to see what works and what doesn't work. (Because there are lessons to be learned from that as well.)

Warning: if you don´t want to read an "adventure style" blog post, and just want to see "how do I do it" you might want to skip to the end. If you´re up for taking part in the frustration of making something work come along for the ride.

This isn't my first foray into identity driven Microsoft setups on this blog, so what makes this different?

If you build a native app you mostly care about acquiring a token. If you build an API you mostly care about validating a token. If you build an MVC-style web app with a mix of API controllers and UI-serving controllers you might have to care about both, but it's a fairly integrated experience from the developer´s perspective since the important things happen on the server where you have all the control you need.

If you build a .NET back-end with a JS front-end you suddenly realize that there are two distinct parts of the authentication dance, and you really need to align them with each other to make it work. Since we're talking about two different programming languages you might also notice discrepancies between the libraries you use in the process.

"Uh, this doesn´t make sense does it? A web app is a web app regardless of it being a SPA or not; you´re still rendering html out of the cloud." Yes, in most cases you don´t have to think further than that. If I write JS code or cshtml markup to present "Hello World" it is pretty much the same thing from the perspective  the end-user. But remember most JavaScript-based apps are not server-based. The scripts are downloaded from a server, but the execution happens locally. Which makes it more like a client-server setup. Which in turn means that token acquisition needs to happen through an OAuth/OpenID Connect flow suited for an untrusted client. (There is of course server-side JavaScript as well, but most of the SinglePage-stuff happens in your browser.)

And lest we forget; while ADFS supports OAuth and OpenID Connect the implementation is not identical to Azure AD.

First attempt
You can find a bunch of samples targeted towards Azure AD here: https://aka.ms/aaddev. Unsurprisingly they focus on AAD, not on-prem ADFS. Which is fair enough really 🙂

Surely ADFS has it's own place for explanations, I quickly learn. This leads me to the following article:
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/development/single-page-application-with-ad-fs

This does a good job of showing what you need to do on your ADFS server for registering your app. Unless you already are intimately familiar with that process you can follow the steps in that article to get the app registration sorted. Unfortunately it will lead you to see an "Authorization has been denied for this request" as already noted in the comments section when trying to list the tasks. You are however able to get a token, so the acquisition part using ADAL JS seems to be working.

This indicates a problem with the token validation part. I don't know why, but the app.UseActiveDirectoryFederationServicesBearerAuthentication lines seems to be the breaking component.

Second attempt
Ok, I have a token, and I have an app registration on my ADFS server. I find a promising sample describing a setup similar to mine with "converged" authentication:
https://github.com/Azure-Samples/active-directory-javascript-singlepageapp-dotnet-webapi-v2

This has a different token validation setup, so let's have a go at it. It uses msal.js which is the newer client library for auth. Build and run…no, that throws up errors in my scripts. Oh, it turns out that msal.js does not support ADFS yet. It will work nicely with Azure AD, but that doesn't really help me now. When support for ADFS is added it will probably be a decent setup though.

Let´s take a slight pause here. The first article referred to ADAL JS, and so did I, but then all of a sudden we switch to talking about MSAL. Maybe this is not familiar turf for everyone. Short version; there are two main libraries from Microsoft - ADAL and MSAL. They are both available in a variety of languages, and if you can use MSAL you probably should. You can refer to my previous article for a more thorough walkthrough:
http://blogs.msdn.microsoft.com/azuredev/2017/05/02/azure-ad-overview-of-libraries/

As already stated MSAL JS does not support ADFS yet however so it is off the table for now, and that is why we will not pursue that path further for now.

Third attempt
Am I able to "transplant" the token validation from the second sample into the first?

Success! I'm finally catching a break. That actually works as intended. (I had some snags that seem to be browser-related - it worked in Internet Explorer and Chrome, but not Edge. Though I have been seeing Edge issues in the Windows 10 Insider builds outside this use case, so there could be a whole range of things messing this up.)

Working sample here: https://github.com/ahelland/AADGuide-CodeSamples/tree/master/ADFSTodoSPA

Quick tip: you need to edit the following files with the FQDN to your ADFS server:
App_Start/Startup.Auth.cs
./Web.config
App/Scripts/app.js

However I originally stated that I wanted it to work with .NET Core. I've tackled Core previously, and if I remember correctly I used "JwtBearerAuthentication" for that. (https://github.com/ahelland/AADGuide-CodeSamples/tree/master/CoreWebAPIServer) So, I create a new project in Visual Studio, copy-paste some code. And it does not work… Wait, I created a .NET Core 2.0 project whereas my original code was for version 1.1. Breaking changes you say?

Yes, I say:
https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x

Fortunately there is a sample provided for this setup:
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore/tree/aspnet_core_2_0

Also made for Azure AD mind you. Luckily the auth part is fairly clean, and can be tweaked to suit an ADFS setup instead.

Change config.json to look similar to this:

{
  "AzureAd": 
    "ClientId": "https://localhost:44326/",
    "Tenant": "adfs",
    "AadInstance": "https://login.contoso.com/{0}",
    "PostLogoutRedirectUri": "https://localhost:44326/"
  }
}

Change accordingly in Startup.cs:

.AddOpenIdConnect(option =>
  {
    option.ClientId = Configuration["AzureAD:ClientId"];
    option.Authority = "https://login.contoso.com/adfs/";
    option.SignedOutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"];
    option.Events = new OpenIdConnectEvents
    {
      OnRemoteFailure = OnAuthenticationFailed,
    };
  });

Lazy me didn´t even rename the naming from AzureAD to ADFS 🙂

So, clearly .NET Core 2 works out as well. And yes, you can reuse the previous Angular parts to create a TodoSPA. In other words the back-end part (validating tokens) should be possible to fix. But as we said earlier apps relying on tokens have two parts, and we still have to make sure the front-end (acquiring token) works outside this narrow scope.

Before moving on we'll allow ourselves to ask a rhetorical question; so far we've put everything into one VS solution - couldn't we split it up? Well, yes, you could. Whether you want to build a more distinct front-end vs back-end setup is potentially a large discussion in itself, so we will not actually go into those considerations. However, it does lead to the question of what you do if you just ignore the whole SPA part of it for the moment, and focus on the API exposed through .NET Core?

For that you can actually follow the wizard in VS 2017 for building a Web API. The wizard once again focuses on Azure AD. Just sign in, and have the code scaffolded for you. Afterwards you need to go Startup.cs and comment out the AddAzureAdBearer part, and throw in something like this:

services.AddAuthentication(sharedOptions =>
{
  sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
  sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
            
.AddJwtBearer(option =>
{
  option.Authority = "https://login.contoso.com/adfs";
  option.Audience = "https://localhost:44326";
});

Make sure that authority and audience is set correctly! And make sure you're running https or you will get a warning about that.

How is this different than the .NET Core code snippet a few paragraphs above? Well, in this case we changed the auth to focus entirely on validating a bearer token, whereas the other one employed an OpenID Connect flow. (I have not found good documentation on all the different options to add for the auth pipeline yet.) So this will only work as an API, not for signing in interactively.

Moving on…

Fourth attempt
There are a bajillion JavaScript frameworks out there for building out a user interface, but since I tried out Angular in the previous attempts lets run with that idea. Could I not just copy in the working Angular/ADAL JS combo into my new .NET Core 2.0 solution and leave it at that? Yes, I could, but that wouldn´t be a challenge would it? (Actually with my JavaScript skills there´s usually some minor detail I have problems with even though it´s a "dummy task".)

The .NET Core 2.0 templates in the latest update to Visual Studio 2017 has a sample based on Angular. As I said in the intro it does not enable the option to use authentication though so you have to plug that in yourself, which given the constraints is perhaps understandable.

Fair enough. But the new templates use a newer version of Angular combined with Webpack. Integrating ADAL JS with this is apparently not that easy,  and I ended up with Webpack errors I was not able to figure out. (From what I can tell there is a component having dependencies to a "something visual" which means it cannot be prerendered. If I´m wrong, and you have a fix do let me know below.)

While trying to figure out this I was made aware of another restriction with ADAL JS:
https://stackoverflow.com/questions/43873379/getting-group-claims-with-adfs-4-0-oauth2-token/43878020

Sigh...

Fifth attempt
At this stage I decided to take things in a different direction - use third-party libraries 🙂

If you've been around the block a time or two you have surely come across Microsoft APIs that only work with a very specific Microsoft component on the client side. Fortunately the thing about something like OpenID Connect is the very fact that it is a standard that "anyone" can implement. (Like all security related code I don't recommend that everyone actually does that.)

Rather than try to hack together something on my own I went with a library provided by people who know what they're doing:
https://github.com/IdentityModel/oidc-client-js

I also found a useful article to set up a sample app:
https://www.scottbrady91.com/Angular/SPA-Authentiction-using-OpenID-Connect-Angular-CLI-and-oidc-client

When you try to wire this up with ADFS it will not work however. Just going by the browser alone you might not even notice, but if you bring up the developer tools of your browser you will see you have an issue with ADFS not being all that happy about your CORS setup.

Fortunately you can get around this by providing some extra configuration parameters, and skip the discovery endpoint. Make sure your UserManagerSettings (in auth.service.ts) looks similar to this:

export function getClientSettings(): UserManagerSettings {
  return {
    authority: 'https://login.contoso.com/adfs/',
    client_id: 'https://localhost:44326/',
    redirect_uri: 'http://localhost:4200/auth-callback',
    post_logout_redirect_uri: 'http://localhost:4200/',
    response_type: "id_token token",
    scope: "openid profile",
    filterProtocolClaims: true,
    loadUserInfo: true,
    metadata:  {
      issuer: 'https://login.contoso.com',
      authorization_endpoint: 'https://login.contoso.com/adfs/oauth2/authorize/',
      userinfo_endpoint: 'https://login.contoso.com/adfs/userinfo',      
      jwks_uri: 'https://login.contoso.com/adfs/discovery/keys'
    }            
  };
}

Lo and behold - you suddenly have a token you can play with! (It also solves the missing claims issue.)

Well, there is one thing, since I didn't really build out the sample properly there is a weird part about the token being part of the query string, but not being passed into the UI correctly, but I consider that a minor issue since ADFS does give me token like it should, and I probably can't blame it for not making the rest of my app work correctly 🙂 Pages are protected, and attempting to open a protected page triggers a redirect to ADFS as we want it to.

Sixth attempt
Getting tired by now? I don´t know what your detective process works like when trying to sort out things you can´t find proper documentation for. But it is common for me to have multiple pieces of code that all work individually, but only solve a specific part of the puzzle. So, this would be about bringing it all together.

So, what would a plan look like? Oh, it´s a straightforward recipe as follows:
• Create a new Visual Studio 2017 solution based on .NET Core 2.0 w/Angular or React.
• Add a token validation step in the .NET pipeline.
• Add oidc-client, and the necessary config.
• Plug oidc-client into the scaffolded JS code generated by the template.
• Profit?

Sounds like a five-minute job right? Now, I know you will feel tempted to call me lazy at this point, but I decided to call it quits. Well, not quits as in "will not use ADFS" or "not touching SPAs again". It's just that without having a preferred JS framework, or a specific app in mind, I didn't feel like going through the effort. Sure, it sounds like a simple thing to say that it's an exercise best left to the reader, but you will likely have to build out some JavaScript on your own anyways since there is no standard way to do it.

You can use ADFS for auth purposes, you can combine it with .NET Core, and with a certain level of motivation you can integrate it with your chosen SPA setup. Ok, so this doesn't really count as attempt number six either, so I take that back 🙂

Comments (2)

  1. This is fantastic walking through the steps with you. Thanks, Andreas!

  2. Sjoukje Zaal says:

    A great in depth article, Andreas!

Skip to main content