I am using ASP.NET Web API. And I do like the ability to add attributes to specify levels of access to the API controllers like this:
[Authorize]
public IEnumerable<Activity> Get()
So far so good, but when I use roles the concept breaks apart.
[Authorize(Roles = "Manager")]
public IEnumerable<Activity> Get()
My user may have logged on to the system a while back, and then at some point they hit a resource that is "forbidden" to them. There is no sense in the user attempting to log on again. Since their rightful account does not have access to that URL. But currently they get a 401 (unauthorized) instead of a 403 (forbidden) as if they had logged on with the wrong account. But the user only has one account, and it is not intended that users ask for an account that belongs to someone else.
Has anyone else dealt with this problem? Does anyone know how to fix this? I am more than willing to write the code to fix this, but I am currently at a loss on where to start.
Web API provides a built-in authorization filter, AuthorizeAttribute. This filter checks whether the user is authenticated. If not, it returns HTTP status code 401 (Unauthorized), without invoking the action. You can apply the filter globally, at the controller level, or at the level of individual actions.
If authentication fails, a 401 Unauthorized response should be returned. Authentication can fail for a lot of reasons: bad password, an expired API token, something in the application changed, etc. For example, a user changing their password often invalidates any oauth tokens issued on behalf of that user.
When Web API calls a method on a controller, it must set values for the parameters, a process called binding. By default, Web API uses the following rules to bind parameters: If the parameter is a "simple" type, Web API tries to get the value from the URI.
Select the Visual C# | Web project type from the project type tree view, then select the ASP.NET MVC 4 Web Application project type. Set the project's Name to ContactManager and the Solution name to Begin, then click OK. In the ASP.NET MVC 4 project type dialog, select the Web API project type. Click OK.
Reading Parv's suggestion, I created the following custom filter called [WebApiAuthorize].
The key is the HandleUnauthorizedRequest() method. When code is executing inside this method, it is because the user is unauthorized "for some reason".... so now we just determine out "why".... and then either:
As you can see, it returns 403 when appropriate (authenticated, but not authorized).
public class WebApiAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(HttpActionContext ctx)
{
if (!ctx.RequestContext.Principal.Identity.IsAuthenticated)
base.HandleUnauthorizedRequest(ctx);
else
{
// Authenticated, but not AUTHORIZED. Return 403 instead!
ctx.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
}
}
}
To use, just throw the custom filter on a controller or action like this.....
[WebApiAuthorize(Roles = "YourRoleA,YourRoleB")]
public class AdminController : ApiController
{
public List<Admin> GetAdmins()
{
...
}
}
I did a bit of research and I coded a solution for me. I found two different Authorize attributes one on System.Web.Mvc and a second one in System.Web.Http. The first one applies to a regular MVC4 app and the second one to the WebAPI portion of MVC4 used for web services including RESTful interfaces. So I used the second one.
I decided to look at the Authorize Attribute Source code at codeplex. And I found this:
protected virtual bool IsAuthorized(HttpActionContext actionContext)
{
if (actionContext == null)
{
throw Error.ArgumentNull("actionContext");
}
IPrincipal user = Thread.CurrentPrincipal;
if (user == null || !user.Identity.IsAuthenticated)
{
return false;
}
if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
{
return false;
}
return true;
}
It is easy to see how authentication and access are conflated by the fact that they both have the same effect of returning false.
Here is a new AuthorizeAttribute I wrote that returns 403 when the user or roles don't match. That way you avoid getting a native log on window. It includes the following code.
if (!IsAuthorized(actionContext))
{
HandleUnauthorizedRequest(actionContext);
}
if (!IsAllowed(actionContext))
{
HandleForbiddenRequest(actionContext);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With