Supporting multiple authentication schemes in asp.net core webapi
Image from Pexels by Laura Gigch

Supporting multiple authentication schemes in asp.net core webapi

Using more than one authentication schemes in webapi projects

Is is not so often than you have to use more than one authentication schemes in your project, but there are some corner cases when you have to do so. One of it is a scenario where you are supporting some weaker authentication schema like basic http authentication where credentials are supplied pretty much on every request.

This kind of loosen security schema is to some limited extend suitable for internal APIs meaning that no other than users or most likely application within your organization network will access. This means no network traffic outside of your organization will reach your service and only trusted clients will access it.

Let's imagine this service at some point has to be exposed to outside world, meaning any client, even the one from outside your network can access it and you are forced to switch to different, more secured authentication schema. However, you still have existing clients that can be different applications coming from different team so switching to new schema may take time and at some point you will have to support both old and new authentication schemes.

This is a rare case because you always should consider that your APIs can be exposed to outside world and that you should take care of the authentication approach at the very beginning but you are probably aware that there are cases when APIs start as internal and eventually , as the whole product evolve, they also evolve and their security approach needs to be reevaluated.

This article will cover some of the keys you should take case of when you are having similar case you need to deal with.

To cut some time on the plumbing and initial setup I used the repository mentioned in Accessing multiple databases from the same DbContext in EF Core article since authentication is a generic thing that can be applied to any application

Implementing custom authentication handlers

Since we are going to stick to simple authentication schemes in this sample, I will create two authentication handlers which are going to handle basic http authentication and api-key authentication schemas. If you want to more about basic-http authentication schema and implementation of it in depth in ASP.NET Core you can check out article Basic authentication with Swagger and ASP.NET Core. In this article I will just briefly go through the handler and Swagger config for basic-http authentication.

Before we start with authentication handlers implementation I will use credentials from configuration file and inject them using options pattern as IOptions injected services.

{
  /*...*/
  "AuthenticationConfig": {
    "Username": "john_smith",
    "Password": "d54f04",
    "Key": "2b1d5be65cd54f0492f4e4504179f39f"
  }
  /*...*/
}
    

To inject these config object I created simple POCO to load config values to

namespace MulitpleDb.Sample.Options
{
    public class AuthenticationConfig
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public string Key { get; set; }
    }
}

    

And finally to register option I added the following line to ConfigureServices in startup.cs class file

        public void ConfigureServices(IServiceCollection services)
        {
            //...
            services.Configure<AuthenticationConfig>(Configuration.GetSection(nameof(AuthenticationConfig)));
            //...
        }
    

These will be credentials that will be used for both authentication schemes

Note

Api-Key schema down the line needs to set the logged in user for the current Http request context, so once key gets validated against the configured key value, username from the config will be used to set currently logged in user for the request http context

Since schema names and header key names will be repeated in multiple places in code, to reduce human error with simple typos I put all of these values to constants which will be use through out the code.

    public static class AuthenticationSchemaNames
    {
        public const string ApiKeyAuthentication = nameof(ApiKeyAuthentication);
        public const string BasicAuthentication = nameof(BasicAuthentication);

        public const string MixSchemaNames = nameof(ApiKeyAuthentication)+","+ nameof(BasicAuthentication);
    }

    public static class HeaderKeyNames
    {
        public const string ApiKeyAuthenticationKey = "X-API-KEY";
    }
    

Basic-http schema handler

Now that we have our credentials configured, we can write the code that will handle each schema. Before any processing is done, the handler will check is anyone is actually logged in to the current http request context and abandon credentials validation

    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        readonly AuthenticationConfig _authenticationConfig;
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IOptions<AuthenticationConfig> authenticationConfig)
            : base(options, logger, encoder, clock)
        {
            _authenticationConfig = authenticationConfig.Value;
        }
        protected override Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            Response.Headers["WWW-Authenticate"] = "Basic";
            return base.HandleChallengeAsync(properties);
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (Request.HttpContext.User.Identity.IsAuthenticated)
                return await Task.FromResult(AuthenticateResult.NoResult());

            if (!string.IsNullOrWhiteSpace(Request.HttpContext.User?.Identity?.Name))
                return await Task.FromResult(AuthenticateResult.NoResult()); //Already authenticated


            if (!Request.Headers.ContainsKey("Authorization"))
                return await Task.FromResult(AuthenticateResult.Fail("Missing Authorization Header"));

            string username = null;
            try
            {
                var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
                username = credentials.FirstOrDefault();
                var password = credentials.LastOrDefault();

                if (!username.Equals(_authenticationConfig.Username) || !password.Equals(_authenticationConfig.Password))
                    throw new ArgumentException("Invalid username or password");
            }
            catch
            {
                return await Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
            }



            var claims = new Claim[] { new Claim(ClaimTypes.Name, _authenticationConfig.Username) };
            var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(claimsIdentity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            var user = new GenericPrincipal(
               identity: claimsIdentity,
               roles: claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray()
               );
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            Request.HttpContext.User = user;

            return await Task.FromResult(AuthenticateResult.Success(ticket));
        }
    }
    

Once user is successfully validated it is set as a current user using ClaimsIdentity with single claim which is username value.

Api-key schema handler

This handler is much simpler as it uses custom headers and it just matches single value. If header supplied key matches, current user is set same as with basic-http schema flow using ClaimsIdentity and it sets single username claim value

    public class TokenAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        readonly AuthenticationConfig _authenticationConfig;
        public TokenAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IOptions<AuthenticationConfig> authenticationConfig)
            : base(options, logger, encoder, clock)
        {
            _authenticationConfig = authenticationConfig.Value;
        }
        
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (Request.HttpContext.User.Identity.IsAuthenticated)
                return await Task.FromResult(AuthenticateResult.NoResult());

            var headerKeyValue = Request.Headers.SingleOrDefault(h => h.Key.Equals(HeaderKeyNames.ApiKeyAuthenticationKey, StringComparison.InvariantCultureIgnoreCase)).Value.SingleOrDefault();

            if(string.IsNullOrEmpty(headerKeyValue) || !headerKeyValue.Equals(_authenticationConfig.Key, StringComparison.InvariantCultureIgnoreCase))
                return await Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));

            var claims = new Claim[] { new Claim(ClaimTypes.Name, _authenticationConfig.Username) };
            var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(claimsIdentity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            var user = new GenericPrincipal(
               identity: claimsIdentity,
               roles: claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray()
               );
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            Request.HttpContext.User = user;

            return await Task.FromResult(AuthenticateResult.Success(ticket));
        }
    }
    

Registering authentication services

We do not want to bloat out service registration method in Startup.cs and for this purpose I always prefer to take out the logic as extension methods

    public static class DependencyInjectionExtensions
    {
        public static AuthenticationBuilder AddApiKeyAuthenticationSchema(this AuthenticationBuilder authentication)
        {
            authentication.AddScheme<AuthenticationSchemeOptions, TokenAuthenticationHandler>(AuthenticationSchemaNames.ApiKeyAuthentication, o => { });
            return authentication;
        }

        public static AuthenticationBuilder AddBasicAuthenticationSchema(this AuthenticationBuilder authentication)
        {
            authentication.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AuthenticationSchemaNames.BasicAuthentication, o => { });
            return authentication;
        }
    }
    

Since we need to add multiple schemes to our application authentication, each method returns it's extending AuthenticationBuilder class instance. This allows to use convenient chaining syntax.

        public void ConfigureServices(IServiceCollection services)
        {
            //...
            services.Configure<AuthenticationConfig>(Configuration.GetSection(nameof(AuthenticationConfig)));
            services.AddAuthentication(o =>
            {
                o.DefaultAuthenticateScheme = AuthenticationSchemaNames.ApiKeyAuthentication;
            })
            .AddApiKeyAuthenticationSchema()
            .AddBasicAuthenticationSchema();

            //...
        }
    

Since anyway all registered authentication handlers will be invoked during the authentication process, setting up DefaultAuthenticateScheme determines which schema and it's handlers will be invoked first during the authentication process.

Adding controller authorization

Since we have more than one authentication schema, it is not enough just to decorate our controllers or controller methods with Authorize attribute. We also need to define schemes we want to challenge credentials against.

Authorize attribute already has constructor which accepts comma separated schema names. We already have this comma separated value as a constant so we'll just use it

    [Authorize(AuthenticationSchemes = AuthenticationSchemaNames.MixSchemaNames)]
    [Route("api/[controller]")]
    [ApiController]
    public class RocketsController : ControllerBase
    {
        [HttpGet("{rocket}")]
        public async Task<String> Get(
            [FromRoute]String rocket,
            [FromQuery]RocketQueryModel query)
        {
            return await Task<String>.FromResult(
                $"Rocket {rocket} launched to {query.Planet} using {Enum.GetName(typeof(FuelTypeEnum), query.FuelType)} fuel type"
                );
        }
    }
    

Adding authentication schemas to Swagger UI

Now final step before we go and test our authentication is to enable testing it though Swagger UI.

Similar case as with dependency injection, you do not want to overcomplicated and bloat service registering method inside Startup.cs, so we create a separate extension methods that will be applied to an instance of SwaggerGenOptions class.

namespace MulitpleDb.Sample.Extensions
{
    public static class SwaggerExtensions
    {
        public static SwaggerGenOptions AddBasicAuthSchemaSecurityDefinitions(this SwaggerGenOptions options)
        {
            options.AddSecurityDefinition("basic", new OpenApiSecurityScheme
            {
                Name = "Authorization",
                Type = SecuritySchemeType.Http,
                Scheme = "basic",
                In = ParameterLocation.Header,
                Description = "Basic Authorization header using the Bearer scheme."
            });

            options.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                          new OpenApiSecurityScheme
                            {
                                Reference = new OpenApiReference
                                {
                                    Type = ReferenceType.SecurityScheme,
                                    Id = "basic"
                                }
                            },
                            new string[] {}
                    }
                });

            return options;
        }

        public static SwaggerGenOptions AddApiKeyAuthSchemaSecurityDefinitions(this SwaggerGenOptions options)
        {
            options.AddSecurityDefinition("token", new OpenApiSecurityScheme
            {
                Name = HeaderKeyNames.ApiKeyAuthenticationKey,
                Type = SecuritySchemeType.ApiKey,
                In = ParameterLocation.Header,
                Description = "Api key from header",
            });


            options.AddSecurityRequirement(new OpenApiSecurityRequirement
                    {
                        {
                            new OpenApiSecurityScheme
                            {
                                Reference = new OpenApiReference {
                                    Type = ReferenceType.SecurityScheme,
                                    Id = "token"
                                }
                            },
                            new string[] {}
                        }
                    });

            return options;
        }
    }
}

    

With these extension method in place we can easily and in a pretty clan way add our authentication schema definitions to Swagger UI which will render out UI for both schemes and we can use it to test our authentication flow

public void ConfigureServices(IServiceCollection services)
{
//...
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MulitpleDb.Sample", Version = "v1" });
c.ParameterFilter<PlanetsParameterFilter>();
     c.AddApiKeyAuthSchemaSecurityDefinitions().AddBasicAuthSchemaSecurityDefinitions();

            }).AddSwaggerGenNewtonsoftSupport();
			
            services.Configure<AuthenticationConfig>(Configuration.GetSection(nameof(AuthenticationConfig)));
			
            services.AddAuthentication(o =>
            {
                o.DefaultAuthenticateScheme = AuthenticationSchemaNames.ApiKeyAuthentication;
            })
            .AddApiKeyAuthenticationSchema()
            .AddBasicAuthenticationSchema();

            //...
        }
    
    

Although not necessary, I also used chaining enables syntax by returning the extended class instance from the extension method. 

We are all setup for fire up our multiple authentication schema enables Web Api instance and test out our schema flows directly from Swagger UI

Multiple Swagger Schemas 

Note

All code snippets are part of the solution you can find in GitHub repository https://github.com/dejanstojanovic/efcore-multiple-db

Now to test our authentication I will insert our api-key from the appsettings.json configuration file and hit "Execute" button in Swagger UI. Is you check the CURL command you will see that X-API-KEY header values is sent.

curl -X 'GET' \
  'https://localhost:5001/api/Rockets/R1?FuelType=Solid&Planet=Mercury' \
  -H 'accept: text/plain' \
  -H 'X-API-KEY: 2b1d5be65cd54f0492f4e4504179f39f'

If we want to check the corner case and have values for both authentication schemes set, you will see all header values and both of authentication handlers will validate the request, but since both handlers check for authenticated user first, only one handler will execute the condition against credentials while the second one (in this case basic-http schema handler) will just skip the authentication logic

 Multiple Swagger Schemas Authenticated

curl -X 'GET' \
  'https://localhost:5001/api/Rockets/R1?FuelType=Solid&Planet=Mercury' \
  -H 'accept: text/plain' \
  -H 'X-API-KEY: 2b1d5be65cd54f0492f4e4504179f39f' \
  -H 'Authorization: Basic am9obl9zbWl0aDpkNTRmMDQ='

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article