I am using ASP.NET MVC 4 Web application as a front-end for some WCF services. All the user log in/log out and session control is done on the back-end. MVC app should only store a single cookie with session ID. My client does not allow to use Forms Authentication, everything must be customized.
I have set up the following in my web.config:
<system.web>
...
<authentication mode="None" />
</system.web>
<system.webServer>
<modules>
...
<remove name="FormsAuthentication" />
...
</modules>
</system.webServer>
I have also a global filter:
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
// Force all actions to request auth. Only actions marked with [AllowAnonymous] will be allowed.
filters.Add(new MyAuthorizeAttribute());
}
}
which is called in Global.asax
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
I have marked with [AllowAnonymous] every controller and action which does not need authorization.
And now I have to implement MyAuthorizeAttribute. I have tried some tutorials, but none of them completely match my scenarios.
Basically, I have to handle the following scenarios for each action:
As far as I understand, I need to override AuthorizeCore (to check my cookie to see if the session exists and is still valid) and HandleUnauthorizedRequest (to redirect users to Home/Index instead of default Login page).
For redirection I tried:
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
base.HandleUnauthorizedRequest(filterContext);
filterContext.Result = new RedirectResult("/Home/Index/NeedsLogin");
}
which seems to handle the scenario 2nd fine (I'm not sure about that base call, though - is it needed?).
For the 1st scenario, I need to implement AuthorizeCore. I'm not sure, how to do it correctly. I have seen that AuthorizeAttribute has some code for handling caching situations and maybe many more hidden functionality and I don't want to break it.
For the 3rd scenario, I am not sure if MyAuthorizeAttribute will be able to handle it. Can AuthorizeAttribute catch exceptions which occur inside of the Action or I'll have to handle SecurityFault.SessionExpired situations in my global error handler?
Right-click on the solution and add a new class. Enter the class name and click on Add. Next Inherite Attribute, IAuthorizationFilter to CustomAuthorization class which has overridden the OnAuthorization method. The OnAuthorization Method has the AuthorizationFilterContext parameter.
Setting an Authentication Filter To apply an authentication filter to a controller, decorate the controller class with the filter attribute. The following code sets the [IdentityBasicAuthentication] filter on a controller class, which enables Basic Authentication for all of the controller's actions.
Here is how I did it for now:
public class MyAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
bool authorized = false;
/// MVC 4 boilerplate code follows
if (filterContext == null)
throw new ArgumentNullException("filterContext");
bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);
if (skipAuthorization)
{
return;
}
if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
{
throw new InvalidOperationException(
"MyAuthorizeAttribute cannot be used within a child action caching block."
);
}
// end of MVC code
// custom code
if (!AuthorizeCore(filterContext.HttpContext))
{
// if not authorized from some other Action call, let's try extracting user data from custom encrypted cookie
var identity = MyEncryptedCookieHelper.GetFrontendIdentity(filterContext.HttpContext.Request);
// identity might be null if cookie not received
if (identity == null)
{
filterContext.HttpContext.User = new GenericPrincipal(new GenericIdentity(""), null);
}
else
{
authorized = true;
filterContext.HttpContext.User = new MyFrontendPrincipal(identity);
}
// make sure the Principal's are in sync - there might be situations when they are not!
Thread.CurrentPrincipal = filterContext.HttpContext.User;
}
// MVC 4 boilerplate code follows
if (authorized)
{
// ** IMPORTANT **
// Since we're performing authorization at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else
{
HandleUnauthorizedRequest(filterContext);
}
//end of MVC code
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
throw new ArgumentNullException("httpContext");
// check to make sure the user is authenticated as my custom identity
var principal = httpContext.User as MyFrontendPrincipal;
if (principal == null)
return false;
var identity = principal.Identity as MyFrontendIdentity;
if (identity == null)
return false;
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// default MVC result was:
// filterContext.Result = new HttpUnauthorizedResult();
// but I redirect to index login page instead of kicking 401
filterContext.Result = new RedirectResult("/Home/Index/NeedsLogin");
}
// MVC 4 boilerplate code follows
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
// This method must be thread-safe since it is called by the caching module.
protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
if (httpContext == null)
throw new ArgumentNullException("httpContext");
bool isAuthorized = AuthorizeCore(httpContext);
return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
}
}
It does not handle my 3rd scenario, though, so I'll implement it in a global error handler.
Not totally sure I get it but if you create an Custom Authorization Filter that inherits from System.Web.MVC.Authorize attribute like this.
public class CustomAuthorize : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (CookieIsValid(filterContext.Request.Cookies["cookieyouwant"])
{
filterContext.Result = new RedirectResult("DestUrl");
}
else
{
filterContext.Result = new RedirectResult("/Home/Index/NeedsLogin");
}
}
}
And then decorate your Methods that need to employ this Authorization will that do the trick?
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