Click here to Skip to main content
15,867,704 members
Articles / Web Development / ASP.NET / ASP.NET Core

Branching Authentication in ASP.NET Core 2.1

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
13 Nov 2018CPOL5 min read 4.4K   1  
Branching authentication in ASP.NET Core 2.1

Introduction

In the last post, I hinted at using SignalR and query strings to branch how a user is authenticated.

Backstory

I ended up in this scenario when one of the requirements for the system was that it needs to be embedded into the client’s system, that means we would have some JavaScript package integrated into their page and it needs to communicate with our system which is a user based system.

The quickest solution (though the least maintainable, only quickest in the least thinking effort to come up with) would be to have 2 separate apps that would treat this independently, our main app and the integration part. Reasons why this wouldn’t be maintainable are the following:

  • Testing time and debugging. When an issue showed up, we would need to increase the workload in maintenance and validate changes to both systems.
  • Lack of scalability. From this approach, if future clients would require a different integration than that would require another application.
  • Automated testing. Sure we could test out the common code but so much would diverge it would be a burden on the system to test out the branching.
  • Environment setup. Since we’re talking about integration and separate applications, there are only so many applications you can have running at the same time and keep your train of thought.

So I set out to find a way of having SignalR and authentication working in such a way that we could maintain our current application, have the same control and behavior both in the main app and the client-integrated side, and for the client side to actually work, it’s more of an opt-in process than anything else.

To get a feel for what was built and how, our control ended up being a React control (this way, it was easier to test locally as well as styling), that communicated via SignalR from a different domain to our user based application.

This is the reason the previous post came to fruition because we wanted to configure our control to reach out to our system, but also let us know which environment it was coming from so that we can decide the authentication.

Now Back to Our Show

So we saw how to send additional data to SignalR via query strings to give us more information about what is expected, next question is, how do we take advantage of that?

Full disclosure, since ASP.NET Core is so extendable and lives in so many different packages, it’s hard to find the source code for a specific class, at least it was for me, so decompilers come in handy, that is not to say that decompilers should be used for malevolent purposes. I use them mostly when I want to understand the underlying system better than whatever documentation if it even exists, that’s my source of truth.

Options Considered

The first thing that came to mind, would have been IdentityServer 4, that worked for a time until we found out that our client required us to authenticate from their systems, and that they are using WCF for it, so that was a bust.

Next, tried with Authentication handlers, that was one deep rabbit hole that didn’t bring a whole lot of value (did I mention the time pressure? 😀 ), not to say that it was a waste or that it wouldn’t work, just too long and too many failed tests to continue on it.

The Solution

Finally, the current approach (there might be a better one in the future, who knows, it’s fun right? :D) was to dive into the AuthenticationMiddleware.

The AuthenticationMiddleware code looks as follows (follow it more behavior-wise than code wise, keep in mind that decompiled code doesn’t always look as it was written originally):

C#
public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider chemes)
    {
        _next = next ?? throw new ArgumentNullException(nameof (next));
        Schemes = schemes ?? throw new ArgumentNullException(nameof (schemes));
    }

    public IAuthenticationSchemeProvider Schemes { get; set; }

    public async Task Invoke(HttpContext context)
    {
        context.Features.Set(new AuthenticationFeature
        {
            OriginalPath = context.Request.Path,
            OriginalPathBase = context.Request.PathBase
        });
        IAuthenticationHandlerProvider handlers = ontext.RequestServices.GetRequiredService();
        foreach (AuthenticationScheme authenticationScheme 
                 in await chemes.GetRequestHandlerSchemesAsync())
        {
            IAuthenticationRequestHandler handlerAsync = await handlers.GetHandlerAsynccontext, 
                                  authenticationScheme.Name) as IAuthenticationRequestHandler;
            bool flag = handlerAsync != null;
            if (flag)
                flag = await handlerAsync.HandleRequestAsync();
            if (flag)
                return;
        }
        AuthenticationScheme authenticateSchemeAsync = 
                                  await chemes.GetDefaultAuthenticateSchemeAsync();
        if (authenticateSchemeAsync != null)
        {
            AuthenticateResult authenticateResult = 
                                  await context.AuthenticateAsyncauthenticateSchemeAsync.Name);
            if (authenticateResult?.Principal != null)
                context.User = authenticateResult.Principal;
        }
        await _next(context);
    }
}

Looking at the code above, the most important line for authentication would be context.User = authenticateResult.Principal; since that actually sets the user to be used throughout the rest of the request.

Sadly, as we can see, the Invoke method is not virtual, the only option remaining was to paste it as is in our codebase, rename it so as to not create confusion and then tweak it to suit our needs.

The change would be something like this:

C#
public class CustomAuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;
    private readonly IServiceProvider _serviceProvider;

    public CustomAuthenticationMiddleware(RequestDelegate next, 
        IAuthenticationSchemeProvider schemes,
        IConfiguration configuration,
        IServiceProvider serviceProvider)
    {
        _next = next ?? throw new ArgumentNullException(nameof (next));
        _configuration = configuration ?? throw new ArgumentNullException(nameofconfiguration));
        Schemes = schemes ?? throw new ArgumentNullException(nameof (schemes));
        _serviceProvider = serviceProvider;
    }

    public IAuthenticationSchemeProvider Schemes { get; set; }

    public async Task Invoke(HttpContext context)
    {
        context.Features.Set(new AuthenticationFeature
        {
            OriginalPath = context.Request.Path,
            OriginalPathBase = context.Request.PathBase
        });

        if (context.Request.Path.Value.Contains("someArbitraryPath")) // and any other 
                                                                      // conditions we wish
        {
            IServiceProvider scopedServiceProvider = _serviceProvider.CreateScope().ServiceProvider;
            IUserClaimsPrincipalFactory claimsPrincipalFactory =
                scopedServiceProvider.GetRequiredService<IUserClaimsPrincipalFactory>();
            ApplicationUser user = new ApplicationUser(); // here you would get the 
                                                          // actual user from your system;
            context.User = await claimsPrincipalFactory.CreateAsync(user);
        }
        else
        {
            IAuthenticationHandlerProvider handlers = ontext.RequestServices.GetRequiredService);
            foreach (AuthenticationScheme authenticationScheme 
                                in await chemes.GetRequestHandlerSchemesAsync())
            {
                IAuthenticationRequestHandler handlerAsync = 
                await andlers.GetHandlerAsync(context, authenticationScheme.Name) 
                                as AuthenticationRequestHandler;
                bool flag = handlerAsync != null;
                if (flag)
                    flag = await handlerAsync.HandleRequestAsync();
                if (flag)
                    return;
            }
            AuthenticationScheme authenticateSchemeAsync = 
                                    await chemes.GetDefaultAuthenticateSchemeAsync();
            if (authenticateSchemeAsync != null)
            {
                AuthenticateResult authenticateResult = 
                                    await context.AuthenticateAsyncauthenticateSchemeAsync.Name);
                if (authenticateResult?.Principal != null)
                    context.User = authenticateResult.Principal;
            }
        }

        await _next(context);
    }
}

In this sample, we injected both the IConfiguration and the IServiceProvider because for our scenario, we needed to have a configuration in place for different environments and also we would want to use our repositories and custom services from the service provider.

One thing to know here is the line where it says _serviceProvider.CreateScope().ServiceProvider; because what I found was that some services could not be created from the root ServiceProvider so for this to work, we would need to create a specific scope to retrieve them.

Things to Keep in Mind

Pros

  • We can introduce any number of custom logic we want, have it configurable and make decisions based on the incoming request, that includes cookies, headers and anything else that might arrive.
  • This won’t affect the rest of the application, from the HttpContext point of view or any controller, the user is just as valid and authenticated as any other.

Cons

Since this is not cookie-based as is the case for normal authentication, you need to make sure that the check you’re doing is quite fast and very specific, like in our case once the SignalR negotiation is done, it’s unlikely that this will run again if it ends up using WebSockets (I assume, I might be wrong).

Conclusion

I know it’s somewhat of a hacky approach though I can say that it does work, especially in the scenario we would be facing.

One of the benefits of ASP.NET Core is the fact that you can define your own pipeline, so why not take advantage of that as I did and make some fun middle ware, step in and experiment with how the system works and make it suit your needs. Might not always be the best approach, but I see less and fewer talks about taking advantage of the middleware systems than it can offer.

Thank you and happy coding.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Romania Romania
When asked, I always see myself as a .Net Developer because of my affinity for the Microsoft platform, though I do pride myself by constantly learning new languages, paradigms, methodologies, and topics. I try to learn as much as I can from a wide breadth of topics from automation to mobile platforms, from gaming technologies to application security.

If there is one thing I wish to impart, that that is this "Always respect your craft, your tests and your QA"

Comments and Discussions

 
-- There are no messages in this forum --