Wednesday, 1 February 2012

Restricting URL access without using the web.config

I’ve been lucky enough to work with 2 major clients that do not use the out-of-the-box user-role association of security. If your site is not restricted by a user’s role or user name, how do you implement security? Good question!

So some examples of custom URL authorization is:

  • Access to a page is not determined solely on a role or username.
  • Allow admins to change web page access permissions on-the-fly from a maintenance page.
  • Allow pages to be restricted via a timeframe. Admin users may still be allowed access after working hours.

An example

Imagine a site that has 4 user roles:

  • Manager
  • Supervisor
  • Employee
  • Administrator

You might have a requirement that:

“An access control page needs to be created so that we (the administrators) can select the permissions of the pages through a UI. One week we might decide to extend a supervisors to a subset of the manager pages. These changes may be permanent or temporary. Either way we need an admin screen to selectively choose the permissions for each page from an admin screen and restrict access this way.”

Equipped with your vast ASP.NET knowledge, you could advise them to create an intermediate role of “Super-supervisor” and use web.config files to restrict user access. Responses are:

  • The existing system embeds the role so tightly, that this requires too much work to implement across the business logic and reporting structure.
  • Changes to web.config to incorporate page access will require direct access to the Web box. Our application’s sys admins are not technical users and could bring down the site.
  • We have already decided to split the site into 3 sections – Public, Secure and Admin sections. However you choose to implement it, this is the only 3 categories we care about.
  • We also want the sitemap to update dynamically with these changes.

So lets see what API we could use …. hmmm … unfortunately:

  • Membership providers only help us identify who a logged in user is – no go.
  • Role providers only help us identify what role a specific user has – no go.

What we need is a way to check the access types allowed upon access to a page. Then restrict page from there.

How does ASP.NET do it?

It does so by use of the UrlAuthorizationModule, which looks through the Web.config locating the page or directory. Then using the defined rules, it will either allow access, or send a 401 (Unauthorized) response to the pipeline. Later on in the pipeline, the FormsAuthenticationModule sees the 401 and pushes them to the login screen.

Good news is works in a very similar way to how we want. Sadly, it isn’t inheritable so we have to roll our own one. So lets have a stab at it.

Rolling our on UrlAuthorizationModule

Modules work by plugging into the HTTP pipeline for all requests. So just implement IHttpModule, add code to the Init method, and hook into the AuthorizeRequest event.

public class CustomUrlAuthorizationModule : IHttpModule
{
private const int Unauthorised = 401;

#region IHttpModule Members

public void Dispose()
{
// Do nothing
}

public void Init(HttpApplication context)
{
context.AuthorizeRequest += new EventHandler(context_AuthorizeRequest);
}

void context_AuthorizeRequest(object sender, EventArgs e)
{
// Work out access from the URL
}

#endregion
}

Nearly there! Okay, so the second-to-last thing you need to do is look at the page coming in, get the information about what users/roles can access that page. So here’s some sample code for it.

void context_AuthorizeRequest(object sender, EventArgs e)
{
var context = HttpContext.Current;

// Use custom logic to determine access criteria
bool accessAllowed = IsUserAuthorizedToSeePage(
context.User.Identity.IsAuthenticated,
context.User.Identity.Name,
context.Request.Path);

if (accessAllowed)
{
return;
}
else
{
// Set status code to 'Unauthorized' and bypass all other components
context.Response.StatusCode = 401;
context.ApplicationInstance.CompleteRequest();
}
}


And lastly, some sample logic to check if a user has access:


private bool IsUserAuthorizedToSeePage(bool isAuthenticated, string userName, string url)
{
using(Data.DbEntities db = new Data.DbEntities())
{
var dbUrl = db.PageAccess.Where(row => row.PageUrl.Equals(url,StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

if(dbUrl == null)
{
logger.Error("Cannot find access rights for {0}", url);
return DenyAccess("Page access rights cannot be found.");
}
else if (dbUrl.Public)
{
return true;
}
else if(!isAuthenticated)
{
logger.Warn("Unauthenticated request for URL {0}",url);
return false;
}
else if(dbUrl.Secure)
{
return true;
}
else if (dbUrl.Admin)
{
var userInfo = db.Users.Where(user => user.UserName == userName).FirstOrDefault();

if (userInfo == null)
{
logger.Error("Cannot find access rights for {0} to {1}", userName, url);
return false;
}
else if (userInfo.Role == "Admin")
{
return true;
}
else
{
logger.Warn("User {0} attempted to access {1}, but was disallowed due to access rights", userName, url);
return false;
}
}
else
{
logger.Error("Unable to determine if user should access page. User: {0} -- URL: {1}", userName, url);
return false;
}
}

And that’s all you need to do really. I pull the role out of the database, but if you have decided to store the users role in Session, you can still pull their roles through from the HttpContext.Current.Session object.


(BTW, the “logger” would be some implementation of a logger, I use NLog)