We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Implementing Custom Authentication Scheme and Handler in ASP.NET Core 3.x

ASP.NET Core  • Posted 4 months ago

Whenever we talk about token authentication, we talk in terms of the most commonly used token authentication mechanisms such as JWT Bearer, Cookie and so on. But in certain scenarios, wherein we would require to authenticate a custom built token be it for internal purposes or for customization purposes, we can't be using these default authentication schemes in order to authenticate an incoming token. In such cases, we would need to build our own authentication scheme which would contain the necessary logic to extract token from the headers, digest it, validate and decide whether the requirement has been successfully met or the token is invalid and is a failure. Let's talk about how we can implement our own authentication scheme of token validation and then decorate an endpoint to see it in action.

Related: Implementing JWT Token Authentication in ASP.NET Core

Scenario and Hands-On:

Let's consider a scenario where we need to validate an incoming token and then extract the claims which are stored in it if the token is valid. For simplicity sake, let's assume that the incoming token is not a JWT standard token and instead a base64 encoded serialized JSON string which contains a set of key value pairs which we assume to be our user claims. Now we can't use a default Bearer scheme for this case, since the token doesn't follow a JWT token format. In such cases we make use of the inbuilt AuthenticationHandler class which is provided as an abstract base class for us to override and implement under Microsoft.AspNetCore.Authentication namespace.

The process is of two steps, first we shall define an options class to specify the scheme we are gonna define, which is basically an extension of the AuthenticationSchemeOptions class provided by the library. Second, we define an AuthenticationHandler which works for this defined AuthenticationScheme under which we shall provide the necessary validation logic and must tell the base authentication middleware whether the incoming token validation is a success or a failure. In case if the token validation is a success, we would need to pass in an AuthenticationTicket generated basing on the claims we create using the data from the token. And this AuthenticationTicket is passed onto the base Authentication middleware which then sets up User claims from the claims we have passed. Finally we attach this AuthenticationScheme and AuthenticationHandler to our Authentication middleware for a constant scheme name we shall provide. And on the endpoint where we would need to invoke this authentication scheme, we shall provide the same under the AuthenticationSchemes attribute.

Now that we are clear with the steps we are going to follow, let's start developing the same in our sample aspnetcore application. For this example, we shall use the latest aspnetcore 3.1 framework and for deserializing the token we shall use ProtoBuf media formatter which we have discussed in a previous article.

Related: Implementing ProtoBuf media formatter on an ASP.NET Core API

First, let's construct the AuthenticationHandler and AuthenticationScheme classes. Let's name our AuthenticationHandler as ValidateHashAuthenticationHandler and the Scheme derivative as ValidateHashAuthenticationSchemeOptions class respectively.


using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace dotnetcore3app.AuthenticationHandlers
{
    public class ValidateHashAuthenticationSchemeOptions
        : AuthenticationSchemeOptions
    { }

    public class ValidateHashAuthenticationHandler
        : AuthenticationHandler<ValidateHashAuthenticationSchemeOptions>
    {
        public ValidateHashAuthenticationHandler(
            IOptionsMonitor<ValidateHashAuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // validation comes in here
        }
    }
}

Observe that the AuthenticationHandler abstract base takes in a type argument of AuthenticationSchemeOptions, which in this case we pass in its derivative ValidateHashAuthenticationSchemeOptions. And then we have an extended constructor which takes several arguments required for token validation, expiry validation and so on which are also passed on to the base class which is AuthenticationHandler in this case. Further we would need to override and provide validation logic within the HandleAuthenticateAsync() method.

Let's start writing the validation logic within the method. We shall begin with the negative cases when there is no token passed under the header which in our case is a custom header "X-Base-Token" and then it is not protobuf deserializable.


	protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // validation comes in here
            if (!Request.Headers.ContainsKey("X-Base-Token"))
            {
                return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
            }

            var token = Request.Headers["X-Base-Token"].ToString();

            try
            {
                // convert the input string into byte stream
                using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(token)))
                {
                    // deserialize stream into token model object
                    var model = Serializer.Deserialize<TokenModel>(stream);
                }
            }
            catch (System.Exception ex)
            {
                Console.WriteLine("Exception Occured while Deserializing: " + ex);
                return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
            }

		//success case happens from here
	}

Now that the token is successfully parsed and the token model has been generated, let's build our claims and then pass over an AuthenticationTicket for the middleware to take care of the rest.

The complete implementation shall look like below:


using System;
using System.IO;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ProtoBuf;

namespace dotnetcore3app.AuthenticationHandlers
{
    public class ValidateHashAuthenticationSchemeOptions
        : AuthenticationSchemeOptions
    { }

    public class ValidateHashAuthenticationHandler
        : AuthenticationHandler<ValidateHashAuthenticationSchemeOptions>
    {
        public ValidateHashAuthenticationHandler(
            IOptionsMonitor<ValidateHashAuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            TokenModel model;

            // validation comes in here
            if (!Request.Headers.ContainsKey("X-Base-Token"))
            {
                return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
            }

            var token = Request.Headers["X-Base-Token"].ToString();

            try
            {
                // convert the input string into byte stream
                using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(token)))
                {
                    // deserialize stream into token model object
                    model = Serializer.Deserialize<TokenModel>(stream);
                }
            }
            catch (System.Exception ex)
            {
                Console.WriteLine("Exception Occured while Deserializing: " + ex);
                return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
            }

            if (model != null)
            {
                // success case AuthenticationTicket generation
                // happens from here

                // create claims array from the model
                var claims = new[] {
                    new Claim(ClaimTypes.NameIdentifier, model.UserId.ToString()),
                    new Claim(ClaimTypes.Email, model.EmailAddress),
                    new Claim(ClaimTypes.Name, model.Name) };

                // generate claimsIdentity on the name of the class
                var claimsIdentity = new ClaimsIdentity(claims,
                            nameof(ValidateHashAuthenticationHandler));

                // generate AuthenticationTicket from the Identity
                // and current authentication scheme
                var ticket = new AuthenticationTicket(
                    new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);

                // pass on the ticket to the middleware
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }

            return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
        }
    }

    public class TokenModel
    {
        public int UserId { get; set; }
        public string Name { get; set; }
        public string EmailAddress { get; set; }
    }
}

Now we shall attach this handler to the authentication middleware under a scheme name. Let's name the scheme name as "ValidateHash". The scheme is augmented onto the middleware as below:



namespace dotnetcore3app
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers()
            .AddProtobufFormatters();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme
                    = AuthenticationSchemeConstants.ValidateHashAuthenticationScheme;
            })
            .AddScheme<ValidateHashAuthenticationSchemeOptions, ValidateHashAuthenticationHandler>
                    (AuthenticationSchemeConstants.ValidateHashAuthenticationScheme, op => { });

	}
	
	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
	{
		// middlewares are added to pipeline here
	}
    }
}

And Finally, we can use this created CustomAuthenticationScheme on any endpoint by using the [Authorize] header as below:


namespace dotnetcore3app.Controllers
{
    [ApiController]
    [Route("[controller]")]

    public class WeatherForecastController : ControllerBase
    {
        [HttpGet]
        [Authorize(AuthenticationSchemes = AuthenticationSchemeConstants.ValidateHashAuthenticationScheme)]
        public IEnumerable<WeatherForecast> Get()
        {
	    // access the claims generated 
            // at the token authentication handler
            string emailAddress = User.Claims.Where(x => x.Type == ClaimTypes.Email)
                                    .FirstOrDefault().Value;
		
		// functionality happens here
        }
    }
}

When we run this, for an incoming token under the header "X-Base-Token" the AuthenticationHandler.HandleAuthenticateAsync() is first invoked for the endpoint which has an Authorize attribute decorated and specified off the AuthenticationScheme as "ValidateHash" and the entire processing takes place. If the token is valid and is parsed successfully, the claims are set which can be read directly under the Endpoint method body. If the token is invalid, we are presented with a 401 UnAuthorized response by default from the Authentication middleware itself.

In this way, we can create a custom authentication scheme and an authentication handler which works whenever the authentication middleware is invoked using the authentication scheme defined.

asp.net core custom authentication handler custom authentication handler asp.net core asp.net core custom authentication scheme custom authentication scheme asp.net core asp.net core custom authentication asp.net core 3 custom authentication asp.net core 3.1 custom authentication .net core custom authentication scheme .net core custom authentication handler asp net core custom authentication custom authentication middleware asp.net core .net core custom authentication .net core 3.1 custom authentication asp.net core 3 custom authentication middleware custom authenticationhandler custom authentication asp.net core custom authentication handler .net core custom authentication middleware asp.net core 2 custom authentication middleware asp.net core custom authentication middleware net core custom authentication aspnet core custom authentication custom authentication .net core asp.net custom authentication .net core 3 custom authentication