2

I am working on ASP.NET Core Web API. I am trying to create a custom Authorize attribute but I am stuck. I could not understand what I am missing. I have the following code for the Authorize attribute and filter:

public class AuthorizeAttribute : TypeFilterAttribute
{
    public AuthorizeAttribute(params string[] claim) : base(typeof(AuthorizeFilter))
    {
        Arguments = new object[] { claim };
    }
}

public class AuthorizeFilter : IAuthorizationFilter
{
    readonly string[] _claim;

    public AuthorizeFilter(params string[] claim)
    {
        _claim = claim;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var IsAuthenticated = context.HttpContext.User.Identity.IsAuthenticated;
        var claimsIndentity = context.HttpContext.User.Identity as ClaimsIdentity;

        if (IsAuthenticated)
        {
            bool flagClaim = false;
            foreach (var item in _claim)
            {
                if (context.HttpContext.User.HasClaim("Role", item))
                    flagClaim = true;
            }

            if (!flagClaim)
            {
                //if (context.HttpContext.Request.IsAjaxRequest())
                    context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; //Set HTTP 403 
                //else
                //    context.Result = new RedirectResult("~/Login/Index");
            }
        }
        else
        {
            //if (context.HttpContext.Request.IsAjaxRequest())
            //{
                context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; //Set HTTP 401 -   
            //}
            //else
            //{
            //    context.Result = new RedirectResult("~/Login/Index");
            //}
        }
        return;
    }
}

I have copied this code from somewhere and commented unnecessary lines.

Here is my controller class where I am trying to put this:

[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
public class JobController : ControllerBase
{
    // GET: api/<JobController>
    [HttpGet]
    [ActionName("GetAll")]
    public List<Job> Get()
    {
        return JobDataLog.GetAllJobQueue();
    }

    // GET api/<JobController>/5
    [HttpGet("{ID}")]
    [ActionName("GetByID")]
    public Job Get(Guid ID)
    {
        return JobDataLog.GetJob(ID);
    }

    // GET api/<JobController>/5
    [HttpGet]
    [ActionName("GetCount")]
    public int GetCount()
    {
        return JobDataLog.GetJobTotal();
    }
}

Also the Configure and ConfigureService methods of Startup.cs

// This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddDistributedMemoryCache();
            services.AddSession(options =>
            {
                options.IdleTimeout = TimeSpan.FromMinutes(60);
            });

            var tokenKey = Configuration.GetValue<string>("TokenKey");
            var key = Encoding.ASCII.GetBytes(tokenKey);
            services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; })
                .AddJwtBearer(x =>
            {
                x.RequireHttpsMetadata = false;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
            });
            services.AddSingleton<IJWTAuthenticationManager>(new JWTAuthenticationManager(tokenKey));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseCookiePolicy();
            app.UseSession();
            app.Use(async (context, next) =>
            {
                var JWToken = context.Session.GetString("JWToken");
                if (!string.IsNullOrEmpty(JWToken))
                {
                    context.Request.Headers.Add("Authorization", "Bearer " + JWToken);
                }
                await next();
            });

            app.UseAuthentication();
            app.UseAuthorization();

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

The problem is that even this controller has the Authorize attribute, all the actions are being called even the Authorize filter invalidates the authorization.

Also when I placed the following code in the OnAuthorization method:

context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);

It blocked the access of all actions, including those which have an AllowAnnoynmous attribute.

Please help me, I have been stuck on this for last 3 hours.

azhar rahi
  • 149
  • 2
  • 11
  • Are you referencing the correct `[Authorize]` attribute? Rename yours and try it: `MyAuthorizeAttribute` for example. – mxmissile Mar 08 '21 at 20:27
  • [Authorize] is referring to correct one, I did not add the Microsoft.AspNetCore.Authorization reference in the file. Also I have just renamed and checked, it shows syntax error, which means reference is correct – azhar rahi Mar 08 '21 at 20:29
  • Curious why you're going around the provided `Authorize` attribute?? I would look into `IAuthorizationRequirement` and `AuthorizationHandler` if it can't do what you want out of the box. – ChiefTwoPencils Mar 08 '21 at 23:29

1 Answers1

1

If you really want to use a custom AuthorizeAttribute, here you go, this works. :)

You'll have a few squiggly lines, but VS will be able to automatically add the using statements.

The original code had multiple problems:

  1. Setting Reponse.StatusCode doesn't actually lead to a response being returned.
  2. HttpContext.User wouldn't be populated in the first place, because ASP.NET Core only attempts to authenticate the user and populate the user's claims/identity if an endpoint is secured with the built-in AuthorizeAttribute. The following code solves this by deriving from AuthorizeAttribute.
  3. In this case the additional filter factory class wasn't needed, since you're not injecting dependencies. Though, if you had to inject, you'd be out of luck I think, because you couldn't derive both from TypeFilterAttribute and AuthorizeAttribute, and the claims list would be always empty.

Working code

public class MyAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    readonly string[] _requiredClaims;

    public MyAuthorizeAttribute(params string[] claims)
    {
        _requiredClaims = claims;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var isAuthenticated = context.HttpContext.User.Identity.IsAuthenticated;
        if (!isAuthenticated)
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        var hasAllRequredClaims = _requiredClaims.All(claim => context.HttpContext.User.HasClaim(x => x.Type == claim));
        if (!hasAllRequredClaims)
        {
            context.Result = new ForbidResult();
            return;
        }
    }
}

You should probably use policies instead

The reason why this works in such a crappy way is that the ASP.NET Core team doesn't want you to write custom Authorize Attributes. See this answer on the subject. The 'proper' way is to create policies, and assign your claim requirements to those policies. But I also think it's silly that authorization is so inflexible and lacking support for basic scenarios.

Leaky
  • 1,703
  • 2
  • 16
  • 24