A common-case scenario during the development of an ASP.NET MVC web application is the need to restrict the access to some web resources to authenticated users only. If our application features an authentication system based on ASP.NET Membership framework (like the ASP.NET Membership Provider or the updated ASP.NET Identity) you can easily fullfill the task by using the AuthorizeAttribute provided in the System.Web.Mvc namespace to only allow specific Users and/or Roles for a whole Controller and/or for a specific ActionResult:
1 2 3 4 5 |
[Authorize(Roles = "Administrators, Operators")] public class HomeController : BaseController { ... } |
1 2 3 4 5 6 7 8 |
public class HomeController : BaseController { [Authorize(Roles = "Administrators, Operators")] public ActionResult Index() { ... } } |
This simple yet effective approach requires the user to authenticate himself using a login mechanism (usually a login form page or view) and that's why it's not really useful when you need to put togheter a RESTful interface and/or a WebService of any kind. The best way to deal with these things is to adopt one of the many authentication mechanisms supported by the HTTP protocol: Basic, Digest, NTLM just to mention some. The most used, yet also the easiest one to blend into a MVC pattern, it's definitely the Basic Authentication.
To implement it in your application just add the following ActionFilter custom attribute to your project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class BasicAuthenticationAttribute : ActionFilterAttribute { public string BasicRealm { get; set; } protected string Username { get; set; } protected string Password { get; set; } public BasicAuthenticationAttribute(string username, string password) { this.Username = username; this.Password = password; } public override void OnActionExecuting(ActionExecutingContext filterContext) { var req = filterContext.HttpContext.Request; var auth = req.Headers["Authorization"]; if (!String.IsNullOrEmpty(auth)) { var cred = System.Text.ASCIIEncoding.ASCII.GetString(Convert.FromBase64String(auth.Substring(6))).Split(':'); var user = new { Name = cred[0], Pass = cred[1] }; if (user.Name == Username && user.Pass == Password) return; } var res = filterContext.HttpContext.Response; res.StatusCode = 401; res.AddHeader("WWW-Authenticate", String.Format("Basic realm=\"{0}\"", BasicRealm ?? "Ryadel")); res.End(); } } |
As you can see the filter checks for the presence of the Authorization request field and acts accordingly:
- If it's there, it checks the given username and password with those expected by the server application.
- If it's not there, it builds a standard 401 Http Response to tell the client that we need some credentials to access that specific resource. This kind of response will make most web browsers prompt the user with an "insert username and password" standard popup form.
The above implementation requires the developer to manually insert the username and password as ActionFilter required parameters but can be easily extended to make it support any authorization mechanism (MembershipProvider, ASP.NET Identity, custom userbase on an external DBMS or file, etc.) by removing the custom constructor and modifying the OnActionExecuting method IF block accordingly.
The BasicAuthenticationActionFilter attribute is just as versatile as the previously mentioned Authorize attribute, meaning that it can be used to put under Basic Authentication a whole controller or a specific ActionResult:
1 2 3 4 5 |
[BasicAuthenticationAttribute("your-username", "your-password", BasicRealm = "your-realm")] public class HomeController : BaseController { ... } |
1 2 3 4 5 6 7 8 |
public class HomeController : BaseController { [BasicAuthenticationAttribute("your-username", "your-password", BasicRealm = "your-realm")] public ActionResult Index() { ... } } |
You’re the man. Thanks
You’re the man. Thanks
Thank you very much for your hint, the procedure described was really-really usefull!
There’s one thing i’d like to mention, hopefully others can fing it usefull:
if
you use the BasicAuthenticationAttribute class in an ASP.NET MVC
controller to lets say upload a file using AJAX then your controller can
get vulnerable to malicious activity.
The reason is that there can
be created a C# DesktopApplication using Webclient.UploadFile from which
application the controller’s methods can be invoked without knowing the
credentials.
This is because in fact the controller refuses
authentication and responses the appropriate (401) Unauthenticated
response BUT the invoked [HttpPost] method of the MVC controller will
get called and run in the background.
In my case the MVC
controller nicely refused to authenticate without the right credentials
given and kept on popping up the basic authentication window, but when i
was creating the desktop application to implement file uploads using
the MVC controller i surprisedly saw that files are getting uploaded to
the server even with wrong credentials given not matching the user/pass i
set in my BasicAuthenticationAttribute class.
My solution was to
extend the BasicAuthenticationAttribute class’ OnActionExecuting method
to throw an Excepction in case of wrong credentials were given.
That
was great i was happy… as far as i was not getting to authenticate to
the MVC controller through web browser. Because in that case when
trying to access the MVC controller through web browser the
BasicAuthenticationAttribute class’ OnActionExecuting method raised the
Exception by default without even popping up the basic realm
authentication window.
To handle this i extended the
BasicAuthenticationAttribute class by one more property called Hard (for
Hard authentication) and setting this property to true if i want to
avoid the authentication window popping up and expecting that
credentials are being sent directly from let’s say a desktop
application, and setting Hard to false if i want the athentication
window to pop up for credentials when controller’s method are accessed
from web browser.
For what it’s worth, there is a majority security flaw with this code. smtpccnt described the symptoms, but didn’t quite explain why it was happening.
The problem is that calling res.End() doesn’t actually stop the controller action from being invoked on the server side. You need to set filterContext.Result to an ActionResult type, or execution will continue past the action filter. For some use cases, this problem is invisible. If you are just trying to prevent a user from seeing a page, then your solution technically works. However, I was trying to protect a web service endpoint that received data. In my case, the data was still pushed through the action method even though the filter was returning a 401 not authorized response. The solution, as given, offered no protection.
The good news is that the fix is simple. Replace the lines that say
res.StatusCode = 401;
res.End();
with
filterContext.Result = new HttpUnauthorizedResult();
This seems to do the trick :)
Thanks a lot for this: I updated the post accordingly and mentioned you in an appropriate comment line.
You’re very welcome. Thank you for writing this filter..it works great now!
Very nice thanks it works wonderfully!
Is there a way to allow the user to “logout” and close the current session?
Oh my word. I want to make love to you.
I need to add just Basic Auth to a particular folder in a MVC website (well, I guess it’s a View or area, not folder?) but doing it the standard way in IIS by disabling anonymous access and enabling Basic Auth does not work if I try it on the actual file system folder that corresponds to the URL path. However we do have simple Basic Auth on our internal dev websites and that works fine. But it doesn’t seem to on individual files/folders.
Anyway, I was thinking I was out of luck when I found this solution and the browser would just keep spinning after the “res.End()” call. This must be due to the fact that we use our own custom MVC CMS to build our websites.
However, adding your fix absolutely did the trick. It did require some slight tweaking, due to our own CMS being used (had to add in a check on filterContext.HttpContext.Request.Path for the particular location I’m looking for, and only run this authorization logic if it matches a certain string). Also had to put this ActionFilter on the method that basically serves up our CMS page Views.
For those wondering, this isn’t a high-security section of a website, that’s why we were ok with Basic Auth in the first place.
Amazing. You rock!
Hi, glad that you like our post! :) Feel free to like us on Facebook & Twitter if you think we’ve fixed your issue!
Best regards,