Multiple authentication schemes in ASP.NET Core 5.0 WebAPI

2.5k Views Asked by At

I have a full set of (ASP.NET Core) web APIs developed in .NET 5.0 and implemented Cookies & OpenIdConnect authentication schemes. After successful authentication (user id and password) with Azure AD, cookie is generated and stores user permissions etc.

Now, I would like to expose the same set of APIs to a third party consumer using API Key based authentication (via api-key in the request headers). I have developed a custom authentication handler as below.

using Microsoft.AspNetCore.Authentication;

namespace Management.Deployment.Securities.Authentication
{
  public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
  {

  }
}


namespace Management.Deployment.Securities.Authentication
{
  public static class ApiKeyAuthenticationDefaults
  {
    public static readonly string AuthenticationScheme = "ApiKey";
    public static readonly string DisplayName = "ApiKey Authentication Scheme";
  }
}

ApiKeyAuthenticationHandler is defined as below, straight forward, if the request headers contain the valid api key then add permissions claim (assigned to the api key) and mark the authentication as success else fail.

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Management.Securities.Authorization;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace Management.Deployment.Securities.Authentication
{
  public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
  {
    private const string APIKEY_NAME = "x-api-key";
    private const string APIKEY_VALUE = "sdflasuowerposaddfsadf1121234kjdsflkj";

    public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
                                       ILoggerFactory logger,
                                       UrlEncoder encoder,
                                       ISystemClock clock) : base(options, logger, encoder, clock)
    {

    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
      string extractedApiKey = Request.Headers[APIKEY_NAME];

      if (!APIKEY_VALUE.Equals(extractedApiKey))
      {
        return Task.FromResult(AuthenticateResult.Fail("Unauthorized client."));
      }

      var claims = new[]
      {
        new Claim("Permissions", "23")
      };

      var claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
      var authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);

      return Task.FromResult(AuthenticateResult.Success(authenticationTicket));

    }
  }
}

I have also defined ApiKeyAuthenticationExtensions as below.

using Microsoft.AspNetCore.Authentication;
using Management.Deployment.Securities.Authentication;
using System;

namespace Microsoft.Extensions.DependencyInjection
{
  public static class ApiKeyAuthenticationExtensions
  {
    public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder)
    {
      return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, x => { });
    }

    public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, Action<ApiKeyAuthenticationSchemeOptions> configureOptions)
    {
      return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, configureOptions);
    }
  }
}

Skimmed version of ConfigureServices() in Startup.cs is here. Please note I have used ForwardDefaultSelector.

public void ConfigureServices(IServiceCollection services)
        {
            IAuthCookieValidate cookieEvent = new AuthCookieValidate();

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
            {
                options.Cookie.Name = ".Mgnt.AspNetCore.Cookies";
                options.ExpireTimeSpan = TimeSpan.FromDays(1);
                options.Events = new CookieAuthenticationEvents
                {
                    OnRedirectToAccessDenied = context =>
                    {
                        context.Response.StatusCode = 403;
                        return Task.FromResult(0);
                    },
                    OnRedirectToLogin = context =>
                    {
                        context.Response.StatusCode = 401;
                        return Task.FromResult(0);
                    },
                    OnValidatePrincipal = cookieEvent.ValidateAsync
                };
                options.ForwardDefaultSelector = context =>
                {
                    return context.Request.Headers.ContainsKey(ApiConstants.APIKEY_NAME) ? ApiKeyAuthenticationDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme;
                };
                options.Cookie.HttpOnly = true;
                options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            })
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => Configuration.Bind(OpenIdConnectDefaults.AuthenticationScheme, options))
            .AddApiKey();

            services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("email");
            });
            

            services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
            services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

        }

The Configure method is as below.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHsts();
            app.Use((context, next) =>
            {
                context.Request.Scheme = "https";
                return next();
            });

            app.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }   

When I send the correct apikey in the request headers, the custom handler is returning success as authentication result and the request is processed further.

But if an incorrect api key is passed, it is not returning the authentication failure message - "Unauthorized client.". Rather, the request is processed further and sending the attached response.

API Response

What changes to be made to resolve this issue so the api returns the authentication failure message - "Unauthorized client." and stops further processing of the request?

1

There are 1 best solutions below

2
On

if you plan to use apikeys, then you are on your own and there is (as far as I know) no built in direct support for API-keys. There is however built in support for JWT based access tokens and I would recommend that you use that as well for external third parties who wants to access your api. Perhaps using client credentials flow.

For some help, see http://codingsonata.com/secure-asp-net-core-web-api-using-api-key-authentication/

I also think you should configure and let the authorization handler be responsible for deciding who can access the services.

see Policy-based authorization in ASP.NET Core