10

I have an ASP.NET Core 2.2 Web API which was working with the Basic Authentication. So far it worked fine and has no troubles. In one of the Controller, one Action Method is Decorated with [AllowAnonymous] to make the User Login, as usual.

[Produces("application/json")]
[Route("user")]
[AllowAnonymous]
[ApiController]
public class LoginController : ControllerBase
{
    private readonly IConfiguration _configuration;
    private readonly IMessagingService _messageService;
    private readonly IBasicAuthenticationService _basicAuthenticationService;
    private readonly string PWAPIBaseUrl;

    public LoginController(IConfiguration configuration, ILogger<LoginController> logger, IMessagingService messagingService, IBasicAuthenticationService authenticationService)
    {
        _configuration = configuration;
        _logger = logger;
        _messageService = messagingService;
        _basicAuthenticationService = authenticationService;
    }

    [HttpGet]
    [AllowAnonymous]
    [Route("login/{username}/{clientID}")]
    public async Task<IActionResult> UserLogin(string username, string clientID)
    {
        // Check the Credentials Manually
        string failReason = "";
        if (!CheckCredentials(out failReason))
        {
            return StatusCode(StatusCodes.Status403Forbidden, userInfo);
        }

        // Load the Roles and UI Preferences ...

    }
}

As the end of .NET Core 2.2 is near, I have tried upgrading to the .NET Core 3.1 and followed the official Migration Guide and made necessary changes. Though the Application started out smoothly, there is one bugging issue which forbids the upgrade.

On the above controller, the [AllowAnonymous] is not ignored and the Authentication is evaluated and thrown out with an error. But the Login method is executed after. This causes Login to break in all the dependent applications. I have tried all the suggestions from Stackoverflow like this, this and this.

Basic Authentication Handler:

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly ILogger<BasicAuthenticationHandler> _logger = null;
    private readonly IBasicAuthenticationService _basicAuthenticationService;

    public BasicAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        UrlEncoder encoder,
        ILoggerFactory loggerFactory,
        ISystemClock clock,
        IBasicAuthenticationService authenticationService)
        : base(options, loggerFactory, encoder, clock)
    {
        _logger = loggerFactory.CreateLogger<BasicAuthenticationHandler>();
        _basicAuthenticationService = authenticationService;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var config = Util.GetConfig();

        if (!Request.Headers.ContainsKey("Authorization"))
        {
            _logger.LogError("No authorization credentials");
            return AuthenticateResult.NoResult();
        }

        if (!Request.Headers.ContainsKey("ClientID"))
        {
            _logger.LogError("Missing header client token");
            return AuthenticateResult.Fail("Missing header client token");
        }

        var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
        if (authHeader.Scheme != "Basic")
        {
            _logger.LogError("Authentication scheme not recognized");
            return AuthenticateResult.Fail("Authentication scheme not recognized");
        }

        var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
        var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
        var username = credentials[0];
        var password = credentials[1];

        string fullname = "";
        string failReason = "";
        bool t = false;

        IPrincipal principal = null;

        // Do Business Validation against the DB

        if (!t) // login failed
        {
            byte[] bEncodedResponse = Encoding.UTF8.GetBytes(failReason);
            await Context.Response.Body.WriteAsync(bEncodedResponse, 0, bEncodedResponse.Length);
            return AuthenticateResult.Fail(failReason);
        }
        else
        {
            var claims = new[]
            {
                new Claim(ClaimTypes.NameIdentifier, username),
                new Claim(ClaimTypes.Name, fullname),
            };

            var identity = new ClaimsIdentity(claims, Scheme.Name);
            principal = principal==null?new ClaimsPrincipal(identity): principal;
            var ticket = new AuthenticationTicket(principal as ClaimsPrincipal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
    }


}

Startup.cs

public class Startup
{
    public Startup(IWebHostEnvironment environment, IConfiguration configuration, ILoggerFactory loggerFactory)
    {
        Environment = environment;
        Configuration = configuration;
        LoggerFactory = loggerFactory;
    }

    public IConfiguration Configuration { get; }
    public ILoggerFactory LoggerFactory { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication("BasicAuthentication")
            .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

        // Adding the Configuration Options -- Extension Methods to Inject Configuration as IOption POCOs
        services.ConfigureAPIOptions(Configuration);

        // configure DI for application services -- Other DI Objects
        services.ConfigureDependencies(Configuration, LoggerFactory);

        Common.APIConfiguration.Current = Configuration;

        services.AddControllers();
        services.AddAuthorization();
        if (Environment.IsDevelopment())
        {
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "My Materials API", Version = "v1" });
            });
        }
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        if (env.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "My Materials API v1");
                c.RoutePrefix = string.Empty;
            });
        }

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

I am still clueless on what I did wrong and I might be missing something in ASP.NET Core 3.1. Please help me in getting this working. Thanks in advance.

EDIT 1:

ServiceExtensions.cs

public static class ServiceExtensions
{
    public static void ConfigureAPIOptions(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddOptions();
        services.Configure<DataSetting>(configuration.GetSection("DataSettings"));
        services.Configure<UrlSetting>(configuration.GetSection("UrlSettings"));
        services.Configure<SiteSettings>(configuration.GetSection("SiteSettings"));
    }

    public static void ConfigureDependencies(this IServiceCollection services, IConfiguration configuration, ILoggerFactory loggerFactory)
    {
        services.AddSingleton<IConfiguration>(configuration);
        services.AddScoped<IBasicAuthenticationService, BasicAuthenticationService>();
        services.AddScoped<IMessagingService>(s => new MessagingServices(configuration, loggerFactory.CreateLogger<MessagingServices>()));
        services.AddHostedService<TimedHostedService>();
    }
}

A Small Kludge for accessing the Configuration, where DI is not possible.

public static class APIConfiguration
{
    public static IConfiguration Current { get; set; }
}
Sathish Guru V
  • 1,417
  • 2
  • 15
  • 39
  • What is the error message do you get?What is the package of your `services.ConfigureAPIOptions` and `services.ConfigureDependencies` and `Common.APIConfiguration.Current`? – Rena Dec 13 '19 at 01:58

1 Answers1

8

I tried this and it really helps me.

private static bool HasAllowAnonymous(AuthorizationFilterContext context)
    {
        var filters = context.Filters;
        for (var i = 0; i < filters.Count; i++)
        {
            if (filters[i] is IAllowAnonymousFilter)
            {
                return true;
            }
        }

        // When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that
        // were discovered on controllers and actions. To maintain compat with 2.x,
        // we'll check for the presence of IAllowAnonymous in endpoint metadata.
        var endpoint = context.HttpContext.GetEndpoint();
        if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
        {
            return true;
        }

        return false;
    }

https://github.com/dotnet/aspnetcore/blob/bd65275148abc9b07a3b59797a88d485341152bf/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs#L236

It was mentioned here https://learn.microsoft.com/en-us/dotnet/core/compatibility/2.2-3.1#authorization-iallowanonymous-removed-from-authorizationfiltercontextfilters

haiduong87
  • 316
  • 3
  • 14
  • 8
    How does this relate to the `AuthenticationHandler`? Where do you receive `AuthorizationFilterContext` from? – ColinM Jun 16 '20 at 11:05
  • I don't think the docs link work. This should be the correct one: https://learn.microsoft.com/en-us/dotnet/core/compatibility/3.0#authorization-iallowanonymous-removed-from-authorizationfiltercontextfilters – Anttu Jan 05 '22 at 09:11