7

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...!!

3 Answers 3

23

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);
}
Sign up to request clarification or add additional context in comments.

3 Comments

This is absolutely the answer.
In my case, I was missing parameter authenticationType of ClaimsIdentity. Apparently, you can write everything, but it does not work if not specified.
There is a ClaimsPrincipal ctor that takes an identity: new ClaimsPrincipal(claimsIdentity);
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>();

2 Comments

Seems more like a workaround. In a Middleware you can surely do whatever you want, but the AuthenticationHandler ensures you are following the expected "pipeline". Here the question is why, despite you return Success in the handler, it becomes un-success. I was looking for that answer here
This solution isn't a workaround, it is another way to apply your Custom Authentication, here is the article including This way: jasonwatmore.com/post/2021/12/20/…
1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.