Post Cover

(UPDATED to .NET 6) Implementing Custom Authentication Scheme in ASP.NET Core

ASP.NET Core Posted Nov 21, 2021

Whenever we implement token authentication for our APIs to enhance security, we generally go for standard token authentication schemes such as JWT Bearer. This authentication requires the input token be in a JWT (JSON Web Token) and follow certain requirements (such as a particular encryption mechanism for the token like SHA256 or RSA).

But sometimes we might come across requirements to authenticate a custom token, may be because of the client practices or recommendations or whatever. In such cases, we can't use the default authentication schemes (such as Bearer or Cookie) in order to validate an incoming token.

We require to build our own authentication handler which contains the necessary logic to extract token from the headers, validate and decide whether the requirement has been successfully met or the token is invalid and is a failure.

In this complete guide, let's look at how we can implement our own authentication scheme of token validation and then decorate an endpoint to see it in action.

Implementing JWT Token Authentication in ASP.NET Core

How do we create a custom Authentication scheme?

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.

We can't use a default Bearer scheme for this case, since the token isn't encrypted and so isn't a valid JWT subject.

We create a custom authentication handler class that extends the abstract AuthenticationHandler class under Microsoft.AspNetCore.Authentication namespace, and register the implementation in the name of our own "authentication scheme".

The whole process can be simplified into the following steps:

  1. 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.
  2. 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.
  3. If the token validation is a success, we need to pass in an AuthenticationTicket generated basing on the claims we create using the data from the token. This is passed onto the base Authentication middleware which then sets up User claims from the claims we have passed.
  4. We attach this AuthenticationScheme and AuthenticationHandler to our Authentication middleware for a constant scheme name we shall provide.
  5. On the endpoint where we 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 / dotnet6 framework

1. Declaring custom AuthenticationSchemeOptions and AuthenticationHandler:

Let's construct the AuthenticationHandler and AuthenticationScheme classes. Let's name our AuthenticationHandler as MyNinjaAuthHandler and the Scheme derivative as MyNinjaAuthSchemeOptions class respectively.

namespace CustomSchemeNinjaApi.Providers.AuthHandlers.Scheme
{
    public class MyNinjaAuthSchemeOptions
        : AuthenticationSchemeOptions
    { }
}

namespace CustomSchemeNinjaApi.Providers.AuthHandlers
{
    public class MyNinjaAuthHandler
        : AuthenticationHandler<MyNinjaAuthSchemeOptions>
    {
        public MyNinjaAuthHandler(
            IOptionsMonitor<MyNinjaAuthSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // handle authentication logic here
        }
    }
}

Observe that the AuthenticationHandler abstract base takes in a type argument of AuthenticationSchemeOptions, which in this case we pass in its derivative MyNinjaAuthSchemeOptions.

We also 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 need to override and provide validation logic within the HandleAuthenticateAsync() method.

2. Implementing 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 Authorization header and then it doesn't satisfy our expectation.

namespace CustomSchemeNinjaApi.Providers.AuthHandlers
{
    public class MyNinjaAuthHandler
        : AuthenticationHandler<MyNinjaAuthSchemeOptions>
    {
        public MyNinjaAuthHandler(
            IOptionsMonitor<MyNinjaAuthSchemeOptions> 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(HeaderNames.Authorization))
            {
                return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
            }

            var header = Request.Headers[HeaderNames.Authorization].ToString();
            var tokenMatch = Regex.Match(header, AuthSchemeConstants.NToken);

            if (tokenMatch.Success)
            {
                // the token is captured in this group
                // as declared in the Regex
                var token = tokenMatch.Groups["token"].Value;

                try
                {
                    // convert the input token down from Base64 into normal
                    byte[] fromBase64String = Convert.FromBase64String(token);
                    var parsedToken = Encoding.UTF8.GetString(fromBase64String);

                    // deserialize the JSON string obtained from the byte array
                    model = JsonConvert.DeserializeObject<TokenModel>(parsedToken);
                }
                catch (System.Exception ex)
                {
                    Console.WriteLine("Exception Occured while Deserializing: " + ex);
                    return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
                }

                // success branch
                // generate authTicket
                // authenticate the request
                
                /* todo */
            }

            // failure branch
            // return failure
            // with an optional message
            return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
        }
    }
}

3. Creating the AuthenticationTicket:

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:

namespace CustomSchemeNinjaApi.Providers.AuthHandlers
{
    public class MyNinjaAuthHandler
        : AuthenticationHandler<MyNinjaAuthSchemeOptions>
    {
        public MyNinjaAuthHandler(
            IOptionsMonitor<MyNinjaAuthSchemeOptions> 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(HeaderNames.Authorization))
            {
                return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
            }

            var header = Request.Headers[HeaderNames.Authorization].ToString();
            var tokenMatch = Regex.Match(header, AuthSchemeConstants.NToken);

            if (tokenMatch.Success)
            {
                // the token is captured in this group
                // as declared in the Regex
                var token = tokenMatch.Groups["token"].Value;

                try
                {
                    // convert the input token down from Base64 into normal
                    byte[] fromBase64String = Convert.FromBase64String(token);
                    var parsedToken = Encoding.UTF8.GetString(fromBase64String);

                    // deserialize the JSON string obtained from the byte array
                    model = JsonConvert.DeserializeObject<TokenModel>(parsedToken);
                }
                catch (System.Exception ex)
                {
                    Console.WriteLine("Exception Occured while Deserializing: " + ex);
                    return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
                }

                // success branch
                // generate authTicket
                // authenticate the request
                if (model != null)
                {
                    // 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(MyNinjaAuthHandler));

                    // 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));
                }
            }

            // failure branch
            // return failure
            // with an optional message
            return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
        }
    }
}

where TokenModel is a class defined as below:

namespace CustomSchemeNinjaApi.Providers.AuthHandlers.Models
{
    public class TokenModel
    {
        public int UserId { get; set; }
        public string Name { get; set; }
        public string EmailAddress { get; set; }
    }
}

4. Registering the Handler and the Scheme:

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

services.AddAuthentication(
    options => options.DefaultScheme = AuthSchemeConstants.MyNinjaAuthScheme)
    .AddScheme<MyNinjaAuthSchemeOptions, MyNinjaAuthHandler>(
        AuthSchemeConstants.MyNinjaAuthScheme, options => { });

5. Decorating the Endpoint controller:

Finally, we decorate the required endpoint with the custom authentication scheme we just created and configured, by using the "Authorize" attribute as below:

using System.Text.Json;
using CustomSchemeNinjaApi.Providers.AuthHandlers.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace CustomSchemeNinjaApi.Controllers
{

    [Route("api/[controller]")]
    public class NinjasController : ControllerBase
    {
        private readonly NinjaModel[] ninjas;

        public NinjasController(IHostEnvironment env)
        {
            var text = System.IO.File.ReadAllText(
                Path.Combine(env.ContentRootPath, @"data/ninja.json"));
            
            ninjas = JsonConvert.DeserializeObject<NinjaModel[]>(text);
        }

        [HttpGet("alive")]
        public string Alive()
        {
            return "Ninja clan is Alive";
        }

        [HttpGet]
        [Authorize(AuthenticationSchemes 
            = AuthSchemeConstants.MyNinjaAuthScheme)]
        public NinjaModel[] Get()
        {
            return ninjas;
        }

        [HttpGet("{id}")]
        [Authorize(AuthenticationSchemes 
            = AuthSchemeConstants.MyNinjaAuthScheme)]
        public NinjaModel Get(int id)
        {
            return ninjas.Where(x => 
                x.Id == id).FirstOrDefault();
        }
    }

    public class NinjaModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Moniker { get; set; }
        public string[] Techniques { get; set; }
    }
}

6. Testing the API authentication:

To test how our API authentication works, let's go ahead and run the application that we've built.

> dotnet clean && dotnet build && dotnet run

Once the API is running in the ports 5000 / 5001 (or whatever ports you've configured) hit the /api/alive endpoint in a browser to check if the application is working fine. It returns the following string:

curl --location --request GET 'https://localhost:5001/api/Ninjas/Alive'

Ninja clan is Alive

Next, we call one of the GET APIs which have been decorated with our AuthenticationScheme and requires a valid token. When we call the GetById() API without a token we have the following result:

curl --location --request GET 'https://localhost:5001/api/Ninjas/1002'

*throws empty response with header 401 UnAuthorized

We are presented with a 401 UnAuthorized response by default from the Authentication middleware itself.

Finally for the success case, we need to pass a JSON String which is base64 encoded. Let's have a JSON string as below:

{"userId":"1001","name":"Hattori Zenzo","emailAddress":"marishitten@ninpo.com"}

We convert this into a Base64 Encoded string. For this we can simply use any online Base64 Encoder, such as the one I used. The output is this:

eyJ1c2VySWQiOiIxMDAwMSIsIm5hbWUiOiJIYXR0b3JpIFplbnpvIiwiZW1haWxBZGRyZXNzIjoibWFyaXNoaXR0ZW5AbmlucG8uY29tIn0=

When we pass this string in our request headers as below, we get a positive response:

curl --location --request GET 'https://localhost:5001/api/Ninjas/1002' \
--header 'Authorization: Ninpo eyJ1c2VySWQiOiIxMDAwMSIsIm5hbWUiOiJIYXR0b3JpIFplbnpvIiwiZW1haWxBZGRyZXNzIjoibWFyaXNoaXR0ZW5AbmlucG8uY29tIn0='

{
    "id": 1002,
    "name": "Gou",
    "moniker": "Shinobi5",
    "techniques": [
        ""
    ]
}

For an incoming token under the Authorization header the AuthenticationHandler.HandleAuthenticateAsync() is first invoked and the entire processing takes place. The token is valid and so it is parsed successfully. The claims are then set, which can be read directly under the Endpoint method body.

Final Thoughts:

We have successfully implemented a whole new authentication scheme that works within the Authorization header and internally uses its own custom authentication handler for validating the token. This helps us in cases when the standard token schemes don't work and presents us with a whole new space for customized authentication.

The code snippets used in this article are a part of Custom Scheme Ninja, a boilerplate solution, built to demonstrate to demonstrate creating and using a custom Authentication Scheme in ASP.NET Core (.NET 6).

Do check out the repository if you're looking for a simple and functioning solution. Please do leave a star if you liked the boilerplate solution.

Author-Image

Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

You can now show your support. 😊

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