Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AllowAnonymous is not ignored in ASP.NET Core custom AuthenticationHandler after migrating from .NET Core 2.2 to 3.1

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; }
}
like image 836
Sathish Guru V Avatar asked Dec 12 '19 17:12

Sathish Guru V


People also ask

What is AllowAnonymous in. net Core?

[AllowAnonymous] bypasses all authorization statements. If you combine [AllowAnonymous] and any [Authorize] attribute, the [Authorize] attributes are ignored. For example if you apply [AllowAnonymous] at the controller level, any [Authorize] attributes on the same controller (or on any action within it) is ignored.

What is Authorize in ASP net?

Authorization refers to the process that determines what a user is able to do. For example, an administrative user is allowed to create a document library, add documents, edit documents, and delete them. A non-administrative user working with the library is only authorized to read the documents.

How Authorize attribute works?

If a user is not authenticated, or doesn't have the required user name and role, then the Authorize attribute prevents access to the method and redirects the user to the login URL. When both Roles and Users are set, the effect is combined and only users with that name and in that role are authorized.

Does [allowanonymous] attribute ignore authorization and authentication flow?

Adding [AllowAnonymous] attribute or filter either to MVC Controller or as metadata to an Endpoint only ignores Authorization flow. It must also ignore Authentication flow to avoid unnecessary calls to Authentication Handlers and Policy Evaluators.

How to create a custom authentication handler in ASP NET Core?

Custom Authentication Handler .Net Core 1 Creating The Project. Lets start by creating new project in Visual Studio using the ASP.NET Core Web Application template. 2 The CustomAuthOptions Class. ... 3 The CustomAuthHandler Class. ... 4 Telling The Framework About Your Custom Handler. ... 5 Testing. ... 6 Testing the code from Postman. ...

What is authentication middleware in ASP NET Core?

The authentication middleware uses the registered authentication handlers to authenticate a user. The registered handlers and their associated configurations are called schemes. In ASP.Net Core, the authentication middleware is added in the Startup class, inside the Configure method.

Can we implement our own token authentication scheme with ASP NET Core?

In this article, let's look at how we can implement our own token authentication scheme and use it with an ASP.NET Core example. Whenever we implement token authentication for our APIs to enhance security, we generally go for standard token authentication schemes such as JWT Bearer.


1 Answers

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

like image 78
haiduong87 Avatar answered Oct 17 '22 12:10

haiduong87