Search code examples
c#asp.net-mvc-5entity-framework-6asp.net-identity-2

Global redirect back to front page if claim is empty or does not exist (NOT identity verification)


Just to be clear: I am NOT talking about claims-based identity validation.

I am building an app in which I make fine use of Identity 2.2 to provide validation. It is sufficient for my needs.

My problem is that once a user logs in, only the first page is widely accessible without storing additional information in the user’s “session”. In particular, when the user clicks on a major internal item (for sake of convenience, let’s call this a “customer module”, the Guid for that customer is stored in a claim held by the user. That way, the user can move from page to page and still have that same customer’s data brought up on every page regardless of what chunk of data the page was meant to display. This claim is only refreshed with something different when they return to the main page and click on another customer.

For security’s sake I would like to ensure that if a claim gets accidentally dropped or set to empty, the user gets shunted back to the main page regardless of where they are in the system, and preferably without having to put code in each and every page action of every controller.

Suggestions? Or am I completely wrong by making use of claims? Because it’s still early enough in the project to make a u-turn if the advantages of a different method are compelling enough.

EDIT:

Just to let people know my solution: Because only one group of people will be accessing this application (the users that interact with companies, this app is to record the interactions and “company information”), I decided to use a base controller. The users would be able to log on and view lists of companies without coming across any page that derived from BaseController, but once they chose a Company to work with, they needed to have Claims set to be able to maintain page-by-page contact with this company’s information. This information would be reset only when they chose a different company, but since there was always a chance that a claim could be disabled, I needed something to automagically redirect them back to the list of companies to re-set the claims. A BaseController that was employed by only those pages where information specific to one company would be displayed was the perfect solution.

A base controller is simple. Just create a controller called BaseController and you’re off to the races. Change any controller that needs to work with this base controller such that they are public class YourOtherController : BaseController.

I initially tried to make an Initialize method to handle everything, but ran into a rather big problem: I was unable to successfully both access and write to my Claims. As in, I was able to either read my claims but not make use of my ClaimWriter extension, or I was able to make use of my ClaimWriter extension but be unable to read claims in the first place. Since Initialize is wayyyy too low in the stack to actually do both these things, I abandoned it and went for an OnActionExecuted method, which ended up being successful. My code ended up being this:

public class BaseController : Controller {
  private ApplicationDbContext db = new ApplicationDbContext();
  protected override void OnActionExecuted(ActionExecutedContext filterContext) {
    base.OnActionExecuted(filterContext);
    var principal = ClaimsPrincipal.Current.Identities.First();
    var company = User.GetClaimValue("CWD-Company");
    var prospect = User.GetClaimValue("CWD-Prospect");
    if(string.IsNullOrEmpty(company)) {
      filterContext.HttpContext.Response.Clear();
      filterContext.HttpContext.Response.Redirect("/");
      filterContext.HttpContext.Response.End();
    }
    if(!string.IsNullOrEmpty(company) && string.IsNullOrEmpty(prospect)) {
      var id = new Guid(company);
      var prospecting = db.Prospecting
        .Where(x => x.CompanyId.Equals(id))
        .Select(x => x.ProspectingId)
        .ToList().SingleOrDefault();
      if(prospecting.Equals(Guid.Empty)) { // null prospecting
        User.AddUpdateClaim("CWD-Prospecting", "");
      } else { // fill prospecting
        User.AddUpdateClaim("CWD-Prospecting", Convert.ToString(prospecting));
      }
    }
  }
}

I am probably going to change the if(prospecting.Equals(Guid.Empty) part of the Prospecting section to automagically create the first entry in the db (with all null values except for the ProspectingId and the CompanyId, of course), but this is what works for now.


Solution

  • That's a fine use of claims you describe, no need a u-turn. What you need is a MVC filter, authorisation filter. Something like this:

    public class MyAuthorisationFilter : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {   
            var principal = HttpContext.Current.User as ClaimsPrincipal;
    
            if(!principal.Claims.Any(c => c.Type == "My Claim Name"))
            {
                // user has no claim - do redirection
                // you need to create 'AuthenticateAgain' route to your table of routes
                // or you can do other means of redirection
                filterContext.Result = new RedirectToRouteResult("AuthenticateAgain", new RouteValueDictionary());
            }
        }
    }
    

    Then you can add it globally in your filters configuration, but you'll have to exclude your authorisation page from this filter. Or apply on per controller basis - whenever this needs to happen. This is very basic form of filter - a lot of checks are stripped out, but it gives a general direction how to proceed.

    Update

    • This is a good article about Authorise attribute.
    • Here use of AllowAnonymous attribute is explained

    The way you use it - depends on your scenario. In most cases when you only expose a login page to the world - it is sufficient to add this attribute as a global filter (see second link, part about RegisterGlobalFilters) and then sprinkle [AllowAnonymous] on top of controllers/actions which should be exposed without authentication.

    Another approach is to have a base controller that has your attribute applied. And then all your controllers inherit from this base controller. This is more sufficient when global filter does not cut it: cases when you expose different pages to different users - think companies and customers. Your controllers for companies will inherit CompaniesBaseController that has [CompaniesAuthFilter] and customers will be inheriting from CustomersBaseController with [CustomersAuthFilter].