Skip Authentication Schemes in ASP.NET Core

Authentication in ASP.NET Core is both powerful and dynamic. It provides you the power to incorporate many different schemes and augment the logged-in security principal. Authentication schemes are how we apply authentication in ASP.NET Core. What if you want to skip authentication schemes in favor of another?

Today we’ll discuss skipping authentication schemes in ASP.NET Core in order to gain control over request authentication. You will find the code for today’s post on Github.

Before we get started…

In a previous post, we discussed authorization. As a refresher, there is a difference between authentication and authorization. Authentication is how the system knows that you are who you say you are. Authorization, on the other hand, is how the system knows that you can do what you are asking to do.

Microsoft has built ASP.NET Core to be extremely extensible. Within that extensibility is the authentication middleware pipeline. We are able to add any number of schemes to the authentication middleware, each of which has an opportunity to append information to the security principal.

Let’s get started

Borrowing from the post about policy-based authorization, let’s go ahead and implement authentication via two schemes: basic authentication and API key. Keep in mind that in a real-world application, you would likely not use Basic Authentication.

services
	.AddAuthentication()
	.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(AuthenticationSchemes.BasicAuthentication, null)
	.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(AuthenticationSchemes.ApiKey, options => Configuration.Bind("App", options));

In the previous code block, we add two authentication schemes. We also declare “Basic Authentication” as our default scheme. This may look familiar if you read the previous post. If not, that’s ok as well. That said, we’re using code from that post as our baseline.

As I mentioned in the introduction, the authentication middleware executes each scheme and gives them an opportunity to authenticate. When successful, they create an authentication ticket and may also interact with the security principal. Inspecting HttpContext.User shows an Identity property. This property represents the primary identity for the request. That said, it also contains an Identities property where it could contain more identities. Additionally, you may notice that HttpContext.User has Claims. Each authentication scheme can potentially modify these claims.

The aim of this post is not to go into great detail about multiple identities, however. Instead, we’re focused on multiple authentication schemes and how you might want to control them.

Skip authentication schemes

That’s why we’re here, right? If I have multiple schemes and each scheme can modify the security principal, it seems logical there will be times I want to skip them. Enter ForwardDefaultSelector. ForwardDefaultSelector allows us to select a scheme that the currently executing scheme should forward requests to. What this means is that when used, the currently executing scheme will be “skipped” and the forwarded scheme is executed in it’s place. I wish to re-emphasize that. In other words, the scheme could potentially be ran n times.

In one particular use case, this meant that claims from the authentication scheme were being added twice to the security principal. Not generally a problem, but it had an interesting side-effect in something we were doing. But I digress.

Setting it up

In order to avoid that particular problem, we can skip an authentication scheme entirely by forwarding it to a “fake” authentication handler. Consider the following updated authentication setup:

services
	.AddAuthentication()
	.AddScheme<NoResultAuthOptions, NoResultAuthenticationHandler>(NoResultAuthOptions.NoResultAuthenticationScheme, null)
	.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(AuthenticationSchemes.BasicAuthentication, null)
	.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(AuthenticationSchemes.ApiKey, options =>
		{
			Configuration.Bind("App", options);
			options.ForwardDefaultSelector =
				context =>
					context.Request.Headers.ContainsKey("Authorization")
						? NoResultAuthOptions.NoResultAuthenticationScheme
						: null;
		});

We add a new NoResultAuthenticationHandler and then configure our ApiKeyAuthenticationHandler to forward to this scheme in the presence of an “Authorization” header. The NoResultAuthenticationHandler does exactly what it sounds like: it returns AuthenticateResult.NoResult().

Can we make them mutually exclusive?

By now you may be asking yourself “why can’t we do that in the Basic Authentication handler as well?” The short answer is you can’t. The longer answer is you can. Confusing, right?

With the approach I show here, you can’t. If my BasicAuthenticationHandler forwards to NoResult in the presence of an ApiKey and my ApiKey forwards in the presence of Authorization header, then both forward and neither execute. That’s the short answer. If you download the code for this post, please look at the integration test: Get_Sample04_Rejects_Non_XServerKey_Async. This test rejects the request even though it has both Authorization and the X-Server-Key header.

But I told you there is a longer answer as well, right? It turns out you can add a PolicyScheme. This PolicyScheme (per Microsoft’s documentation) makes it:

  • Easy to forward authentication action to another scheme.
  • Forward dynamically based on the request.

The documentation for ForwardDefaultSelector says it executes the first non-null result. This takes it to the next level.

Rather than write up my own post about it, I invite you to jump over to John Reilly’s blog and read about Dual boot authentication with ASP.NET. Further credits to Barbara and her StackOverflow answer which inspired John’s post.

Conclusion

Microsoft has given us myriad ways to extend .NET Core including the authentication middleware. We can easily add any number of schemes for authenticating into our applications. We can exert additional control over those schemes by forwarding requests to another scheme. In this post, we looked at one way to skip authentication schemes. I also showed you another method (by way of John Reilly) to accomplish this.

Credits

Photo by Markus Spiske on Unsplash