'returns 403(forbidden) when using authentication scheme in .net core

I am doing authentication in my web application with JWT Security Tokens and a custom authentication scheme.

  1. I'm generating tokens when a user login

  2. I created an authentication handler where I validate tokens for all requests

Authentication Handler

public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationOptions>
{
    public CustomAuthenticationHandler(
        IOptionsMonitor<CustomAuthenticationOptions> options,
        ILoggerFactory logger, 
        UrlEncoder encoder, 
        ISystemClock clock)
    : base(options, logger, encoder, clock)
    {

    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        try
        {
            Exception ex;
            var key = Request.Headers[Options.HeaderName].First();

            if (!IsValid(key, out ex))
            {

                return Task.FromResult(AuthenticateResult.Fail(ex.Message));
                //filterContext.Result = new CustomUnauthorizedResult(ex.Message);
            }
            else
            {
              
                AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(),new AuthenticationProperties(),this.Scheme.Name);
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
        }
        catch (InvalidOperationException)
        {
            return Task.FromResult(AuthenticateResult.Fail(""));
        }
    }
}

Custom Authentication Extension

public static class CustomAuthenticationExtensions
{
    public static AuthenticationBuilder AddCustomAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CustomAuthenticationOptions> configureOptions)
    {
        return builder.AddScheme<CustomAuthenticationOptions, CustomAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
    }
}

Integrate Custom Authentication into Startup

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        services.AddMvc().AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Person>());
        services.AddTransient<IRepositoryWrapper, RepositoryWrapper>();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddAuthentication(options=> {
            options.DefaultScheme = "CustomScheme";
            options.DefaultAuthenticateScheme = "CustomScheme";
            options.DefaultChallengeScheme = "CustomScheme";
        }).AddCustomAuthentication("CustomScheme", "CustomScheme", o => { });

    }


    public void Configure(IApplicationBuilder app, IHostingEnvironment env,ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole();
        loggerFactory.AddDebug(LogLevel.Information);

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseAuthentication();
        app.UseHttpsRedirection();
        app.UseMvc();

    }
}

Use Authentication scheme within Controller

    [Authorize(AuthenticationSchemes ="CustomScheme")]
    [ApiController]
    [Route("api/controller")]
    public class UserController : BaseController
    {
        
        public UserController(IRepositoryWrapper repository) : base(repository)
        {
        }
        
        [HttpGet]
        public IEnumerable<Users> Get()
        {
            return _repository.Users.FindAll();
        }
    }

When I call the API from Postman with a valid token it returns a 403 error.

Please help to solve this...!!



Solution 1:[1]

I found the solution.

I used CustomAuthenticationMiddle class instead of AuthenticationHandler class

public class CustomAuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    public CustomAuthenticationMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task Invoke(HttpContext context)
    {
            try
            {
                Exception ex;
                var key = context.Request.Headers["Authorization"].First();

                if (!IsValid(key))
                {
                       //logic for authentication
                }
                else
                {

                    await _next.Invoke(context);
                }
            }
            catch (InvalidOperationException)
            {
                context.Response.StatusCode = 401; //Unauthorized
                return;
            }
        }

    private bool IsValid(string key)
    {
        //Code for checking the key is valid or not
    }
}

Replace All above Code for Integrate Custom Authentication Handler with the next line under Startup class

app.UseMiddleware<CustomAuthenticationMiddleware>();

Solution 2:[2]

For anyone else running into this problem:

The original issue appears to be that a ClaimsIdentity was not passed to the ClaimsPrincipal when returning the AuthenticationResult in AuthenticationHandler. Note also that the authentication type must be passed to the ClaimsIdentity, or IsAuthenticated will be false. Doing something like this within the AuthenticationHandler should fix the issue:

else
{
    var claimsPrincipal = new ClaimsPrincipal();
    var claimsIdentity = new ClaimsIdentity("JWT");
    claimsPrincipal.AddIdentity(claimsIdentity);

    var ticket = new AuthenticationTicket(claimsPrincipal, this.Scheme.Name);

    return AuthenticateResult.Success(ticket);
}

Solution 3:[3]

I would like to Explain the two answers to this Question as Both are correct.

About First Answer By @mskowron:

Is the solution for 403 Error in case using the same Custom Authentication Handler Implementation under the Question as questioner missing for define Authentication Schema type under ClaimIdentity instance initialization, Here is a full implementation of CustomAuthenticationHandler class with fix bug

public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationOptions>
{
    public CustomAuthenticationHandler(
        IOptionsMonitor<CustomAuthenticationOptions> options,
        ILoggerFactory logger, 
        UrlEncoder encoder, 
        ISystemClock clock)
    : base(options, logger, encoder, clock)
    {

    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        try
        {
            var key = Request.Headers[Options.HeaderName].First();
            
            Exception vaildationException;
            if (!IsValid(key, out vaildationException))
            {
                return Task.FromResult(AuthenticateResult.Fail(vaildationException.Message));
            }
            else
            {
                // ----->> Here Changes
                var claims = new[] {}; // define claims what's you need
                var identity = new ClaimsIdentity(claims, "JWT");
                var claimsPrincipal = new ClaimsPrincipal(identity);
                AuthenticationTicket ticket = new AuthenticationTicket(claimsPrincipal, this.Scheme.Name);

                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
        }
        catch (InvalidOperationException exception)
        {
            return Task.FromResult(AuthenticateResult.Fail(""));
        }
    }
}

Here is An article for the same solution, But for Basic Authentication

About Second Answer By Who Ask Question:

Is another Way to Implement A Custom Authentication Different from an already exists in Question By depends on implementing a custom Authentication Middleware instead of depending on the default Dotnet Core Authentication Middleware.

Here is An Article for this solution approach, But for Basic Authentication.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ahmed hamdy
Solution 2 mskowron
Solution 3 ahmed hamdy