I'm working on an ASP.NET Core MVC application and I want to be able to better handle what happens when an unauthenticated user runs an action through AJAX.
I found this solution that essentially extends the Authorize attribute's logic and sets an AJAX request's status code to 401 when user is no longer authenticated. The status code is returned to a global AJAX error handler and the appropriate action can be performed.
I'm trying to create the attribute in Core 3.1 but I cannot find a way to first run the base logic of the filter. base.OnAuthorization method is not there. I specifically don't want to rewrite the Authorize attribute's functionality - only to extend it.
How can I extend the Authorize attribute's logic in Core 3.1?
The authorize attribute I'm writing:
public class AuthorizeUserAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
base.OnAuthorization(context); //method does not exist
OnAjaxAuthorization(context);
}
internal void OnAjaxAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User.Identity.IsAuthenticated == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
}
}
}
Just as a side note, I was thinking of writing a ActionFilterAttribute to run the additional code instead, but I can't do that because the Authorize attribute always runs first.
I have created a custom DNN module which uses routing for some functionality.
I register a single route with the pattern: Data/{table}/{action}.aspx (I use a class implementing DNN's IServiceRouteMapper, which has a RegisterRoutes method which is called as DNN starts up).
The route is registered successfully and can be seen in the RouteTable on every page request.
However, every request that should be matched to the route results in DNN showing its 404 error page. In other words, DNN seems to be deciding that the request URL ought to match a DNN page, but doesn't.
If I change the friendly URL provider configuration from urlFormat="advanced" to urlFormat="searchfriendly", then the routing works successfully.
My conclusion is that the DNN friendly URL provider (which is implemented as an HTTP module) intercepts the request first (i.e. before route matching can kick in), and when in "advanced" mode assumes it is a page URL, then tries to find a matching page in the CMS, and fails. However in "searchfriendly" mode it lets it go so route matching can look at the request.
I want to keep the friendly URL provider in "advanced" mode (because the URLs are much cleaner). I also want to find some simple solution that doesn't involve e.g. writing my own friendly URL provider!
In the web.config, URL routing appears to be in the pipeline before the friendly URL provider, so I am confused why the above happens:
<modules runAllManagedModulesForAllRequests="true">
<remove name="UrlRoutingModule-4.0" />
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="managedHandler" />
<add name="RequestFilter" type="DotNetNuke.HttpModules.RequestFilter.RequestFilterModule, DotNetNuke.HttpModules" preCondition="managedHandler" />
<add name="UrlRewrite" type="DotNetNuke.HttpModules.UrlRewriteModule, DotNetNuke.HttpModules" preCondition="managedHandler" />
EDIT: adding the IServiceRouteMapper implementation as requested, i.e. where the route gets registered. Navigator.SiteRootFolder is a constant that returns "Data".
public void RegisterRoutes(IMapRoute mapRouteManager)
{
// This method is called automatically by DNN on startup
RegisterRoutes(RouteTable.Routes);
}
private static void RegisterRoutes(RouteCollection routes)
{
if (Global.CanCreateDataContext())
{
Global.MetaModel.RegisterContext(
new EFDataModelProvider(() => Global.CreateDataContext(Global.Context.DataContextAssemblyLocation)),
new ContextConfiguration { ScaffoldAllTables = Global.Context.ScaffoldAllTables });
Global.MetaModel.DynamicDataFolderVirtualPath = string.Format("~/{0}/DynamicData", Navigator.SiteRootFolder);
routes.Add(
new DynamicDataRoute(string.Format("{0}/{{table}}/{{action}}.aspx", Navigator.SiteRootFolder))
{
Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
Model = Global.MetaModel
});
}
}
That is really a non-standard way of creating services in DNN. Normally you would use a simple route mapper that implements DotNetNuke.Web.Api.IServiceRouteMapper like this:
public class ServiceRouteMapper : IServiceRouteMapper
{ public void RegisterRoutes(IMapRoute mapRouteManager)
{
mapRouteManager.MapHttpRoute(
moduleFolderName: "MyModule",
routeName: "default",
url: "{controller}/{action}",
namespaces: new[] { "MyCompany.Dnn.MyModule.Controllers" });
}
}
This would result in the following route:
/DesktopModules/MyModule/Api/Data/List (assuming you had a controller class named "DataController" and a method named "list".
The reason this doesn't interfere with DNN's Friendly Url provider is because "/DesktopModules" is a reserved path ignored by the Url provider.
Now, if you insist on having the completely custom routes as you have, you could add an path ignore to the Url Provider. To do this, you need to add (or update if it exists) a record in the HostSettings table with SettingName = "AUM_DoNotRewriteRegEx". Below is an example script that can add the path "/Data" to the ignore list.
IF NOT EXISTS (SELECT * FROM HostSettings WHERE SettingName = 'AUM_DoNotRewriteRegEx' )
BEGIN
INSERT INTO HostSettings (SettingName, SettingValue, SettingIsSecure, CreatedByUserId, CreatedOnDate, LastModifiedByUserId, LastModifiedOnDate)
VALUES('AUM_DoNotRewriteRegEx','/DesktopModules/|/Providers|/LinkClick\.aspx|/Data', 0, -1, GETDATE(), -1, GETDATE())
END
ELSE
BEGIN
UPDATE HostSettings SET SettingValue = (select SettingValue + '|/Data' from HostSettings where settingname = 'AUM_DoNotRewriteRegEx')
where SettingName = 'AUM_DoNotRewriteRegEx'
END
I'm using the ASP.Net WebAPI nightly build (2013-01-16) to get the latest OData support possible.
As the Meta-Me blog on MSDN OData 0.2.0-alpha release post says, there's now an EntitySetController<T> from which OData controllers can be derived to take away a lot of the pain and plumbing code.
The EntitySetController<T> class implements Get() as
[Queryable]
public virtual IQueryable<TEntity> Get()
{
throw EntitySetControllerHelpers.GetNotImplementedResponse(Request);
}
I'd like to make use of the more specific Get(ODataQueryOptions options) method offered by the ASP.Net Web API OData support.
I've coded it as
public IEnumerable<Patient> Get(ODataQueryOptions options)
{
IQueryable patients = entities.Patients;
if (options.Filter != null)
{
patients = options.Filter.ApplyTo(patients, new ODataQuerySettings());
}
return (patients as IQueryable<Patient>).AsEnumerable();
}
(I've also had this return IQueryable<> and saw someone else talk about an ODataResult - that's a type I can't discover at the moment).
However, if I try to use the ODataQueryOptions-based Get method in my own controller I get an error message about multiple actions matching the request. Specifically that error is
Multiple actions were found that match the request:
System.Collections.Generic.IEnumerable`1[Dox.Server.Model.Patient] Get(System.Web.Http.OData.Query.ODataQueryOptions) on type Dox.Server.Web.Controllers.PatientController
System.Linq.IQueryable`1[Dox.Server.Model.Patient] Get() on type System.Web.Http.OData.EntitySetController`2[[Dox.Server.Model.Patient, Dox.Server.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
I assume this is due to the route resolver (sorry if that's poor ASP.NET routing terminology) seeing Get() or Get(...) on the controller's base class as well as the controller class itself.
Questions:
a) Is there some way of adjusting routes to fix this?
b) If not, should I make my own version of EntitySetController<T> and swap out it's Get() method?
The configuration being called by Application_Start() is limited to
public static void EnableOData( HttpConfiguration config )
{
var model = BuildModelImplicitly(config);
//As per LinqPad forum: http://forum.linqpad.net/discussion/178/odata-v3-not-working
IEdmEntityContainer container = model.EntityContainers().First();
model.SetIsDefaultEntityContainer(container, true);
//config.EnableOData(model, "api");
config.Routes.MapODataRoute("OData", "api", model);
//config.EnableSystemDiagnosticsTracing();
}
There's no other configuration being called to do with routes or handlers, etc. Note that the EnableOData() method on HttpConfiguration no longer exists in the latest nightly builds as per this CodePlex discussion.
Thanks very much!
It's very cool to see that you're using our nightly builds :)
The reason you're getting a multiple matching actions error is because EntitySetController already defines a Get method. The good news is that EntitySetController also defines a QueryOptions property that you can use to retrieve the query options. So you should be able to override the EntitySetController's Get method and use the query options property instead of the parameter. It should behave exactly the same way as if you had bound the query options to an action parameter.
Using Windows Azure's Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider as the outputCache provider for an MVC3 app. Here is the relevant action method:
[ActionName("sample-cached-page")]
[OutputCache(Duration = 300, VaryByCustom = "User",
Location = OutputCacheLocation.Server)]
[Authorize(Users = "me#mydomain.tld,another#otherdomain.tld")]
public virtual ActionResult SampleCachedPage()
{
return View();
}
I get the following exception when loading this view from a web browser:
System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks.
System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks.
at System.Web.Caching.OutputCache.InsertResponse(String cachedVaryKey, CachedVary cachedVary, String rawResponseKey, CachedRawResponse rawResponse, CacheDependency dependencies, DateTime absExp, TimeSpan slidingExp)
at System.Web.Caching.OutputCacheModule.OnLeave(Object source, EventArgs eventArgs)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
If I remove the [Authorize] attribute, the view caches as would be expected. Does this mean I cannot put [OutputCache] on an action method that must have [Authorize]? Or, do I need to override AuthorizeAttribute with a custom implementation that uses a static validation callback method for the cache?
Update 1
After Evan's answer, I tested the above action method in IIS Express (outside of Azure). Here is my override for the VaryByCustom = "User" property on the OutputCache attribute:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
return "User".Equals(custom, StringComparison.OrdinalIgnoreCase)
? Thread.CurrentPrincipal.Identity.Name
: base.GetVaryByCustomString(context, custom);
}
When I visit the sample cached page as me#mydomain.tld, the output of the page is cached, and the view displays "This page was cached at 12/31/2011 11:06:12 AM (UTC)". If I then sign out and sign in as another#otherdomain.tld and visit the page, it displays "This page was cached at 12/31/2011 11:06:38 AM (UTC)". Signing back in as me#mydomain.tld and revisiting the page causes the cache to display "This page was cached at 12/31/2011 11:06:12 AM (UTC)" again. Further sign in/out attempts show that different output is being cached & returned depending on the user.
This is leading me to believe that the output is being cached separately based on the user, which is the intention with my VaryByCustom = "User" setting & override. The problem is that it doesn't work with Azure's distributed cache provider. Evan, does you answer about only caching public content still stand?
Update 2
I dug up the source, and found that the out-of-box AuthorizeAttribute does in fact have a non-static validation callback. Here is an excerpt from OnAuthorization:
if (AuthorizeCore(filterContext.HttpContext)) {
// ** 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);
}
CacheValidationHandler delegates the cache validation to protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase), which of course is not static. One reason why it is not static is because, as noted in the IMPORTANT comment above, it invokes protected virtual bool AuthorizeCore(HttpContextBase).
In order to do any of the AuthorizeCore logic from a static cache validation callback method, it would need to know the Users and Roles properties of the AuthorizeAttribute instance. However there doesn't seem to be an easy way to plug in. I would have to override OnAuthorization to put these 2 values into the HttpContext (Items collection?) and then override OnCacheAuthorization to get them back out. But that smells dirty.
If we are careful to use the VaryByCustom = "User" property in the OutputCache attribute, can we just override OnCacheAuthorization to always return HttpValidationStatus.Valid? When the action method does not have an OutputCache attribute, we would not need to worry about this callback ever being invoked, correct? And if we do have an OutputCache attribute without VaryByCustom = "User", then it should be obvious that the page could return any cached version regardless of which user request created the cached copy. How risky is this?
Caching happens before the Action. You will likely need to customize your authorization mechanics to handle cache scenarios.
Check out a question I posted a while back - MVC Custom Authentication, Authorization, and Roles Implementation.
The part I think would help you is a custom Authorize Attribute who's OnAuthorize() method deals with caching.
Below is a code block for example:
/// <summary>
/// Uses injected authorization service to determine if the session user
/// has necessary role privileges.
/// </summary>
/// <remarks>As authorization code runs at the action level, after the
/// caching module, our authorization code is hooked into the caching
/// mechanics, to ensure unauthorized users are not served up a
/// prior-authorized page.
/// Note: Special thanks to TheCloudlessSky on StackOverflow.
/// </remarks>
public void OnAuthorization(AuthorizationContext filterContext)
{
// User must be authenticated and Session not be null
if (!filterContext.HttpContext.User.Identity.IsAuthenticated || filterContext.HttpContext.Session == null)
HandleUnauthorizedRequest(filterContext);
else {
// if authorized, handle cache validation
if (_authorizationService.IsAuthorized((UserSessionInfoViewModel)filterContext.HttpContext.Session["user"], _authorizedRoles)) {
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object o, ref HttpValidationStatus status) => AuthorizeCache(context), null);
}
else
HandleUnauthorizedRequest(filterContext);
}
}
/// <summary>
/// Ensures that authorization is checked on cached pages.
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public HttpValidationStatus AuthorizeCache(HttpContext httpContext)
{
if (httpContext.Session == null)
return HttpValidationStatus.Invalid;
return _authorizationService.IsAuthorized((UserSessionInfoViewModel) httpContext.Session["user"], _authorizedRoles)
? HttpValidationStatus.Valid
: HttpValidationStatus.IgnoreThisRequest;
}
I've come back to this issue and, after a bit of tinkering, have concluded that you cannot use the out of the box System.Web.Mvc.AuthorizeAttribute along with the out of the box System.Web.Mvc.OutputCacheAttribute when using the Azure DistributedCache. The main reason is because, as the error message in the original question states, the validation callback method must be static in order to use it with Azure's DistributedCache. The cache callback method in the MVC Authorize attribute is an instance method.
I went about trying to figure out how to make it work by making a copy of the AuthorizeAttribute from the MVC source, renaming it, hooking it up to an action with OutputCache connected to Azure, and debugging. The reason the cache callback method is not static is because, in order to authorize, the attribute needs to check the HttpContext's User against the Users and Roles property values that are set when the attribute is constructed. Here is the relevant code:
OnAuthorization
public virtual void OnAuthorization(AuthorizationContext filterContext) {
//... code to check argument and child action cache
if (AuthorizeCore(filterContext.HttpContext)) {
// 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);
}
}
Cache Validation Callback
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;
}
As you can see, the cache validation callback ultimately invokes AuthorizeCore, which is another instance method (protected virtual). AuthorizeCore, which was also called during OnAuthorization, does 3 main things:
Checks that the HttpContextBase.User.Identity.IsAuthenticated == true
If the attribute has a non-empty Users string property, checks that the HttpContextBase.User.Identity.Name matches one of the comma-separated values.
If the attribute has a non-empty Roles string property, checks that the HttpContextBase.User.IsInRole for one of the comma-separated values.
AuthorizeCore
// This method must be thread-safe since it is called by the thread-safe
// OnCacheAuthorization() method.
protected virtual bool AuthorizeCore(HttpContextBase httpContext) {
if (httpContext == null) {
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!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;
}
When you simply try to make the validation callback method static, the code won't compile because it needs access to these _rolesSplit and _usersSplit fields, which are based on the public Users and Roles properties.
My first attempt was to pass these values to the callback using the object data argument of the CacheValidateHandler. Even after introducing static methods, this still did not work, and resulted in the same exception. I was hoping that the object data would be serialized, then passed back to the validate handler during the callback. Apparently this is not the case, and when you try to do this, Azure's DistributedCache still considers it a non-static callback, resulting in the same exception & message.
// this won't work
cachePolicy.AddValidationCallback(CacheValidateHandler, new object() /* data */);
My second attempt was to add the values to the HttpContext.Items collection, since an instance of HttpContext is automatically passed to the handler. This didn't work either. The HttpContext that is passed to the CacheValidateHandler is not the same instance that existed on the filterContext.HttpContext property. In fact, when the CacheValidateHandler executes, it has a null Session and always has an empty Items collection.
// this won't work
private void CacheValidateHandler(HttpContext context, object data,
ref HttpValidationStatus validationStatus) {
Debug.Assert(!context.Items.Any()); // even after I put items into it
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
However...
Even though there seems to be no way to pass the Users & Roles property values back to the cache validation callback handler, the HttpContext passed to it does in fact have the correct User Principal. Also, none of the actions where I currently want to combine [Authorize] and [OutputCache] ever pass a Users or Roles property to the AuthorizeAttribute constructor.
So, it is possible to create a custom AuthenticateAttribute which ignores these properties, and only checks to make sure the User.Identity.IsAuthenticated == true. If you need to authenticate against a specific role, you could also do so and combine with OutputCache... however, you would need a distinct attribute for each (set of) Role(s) in order to make the cache validation callback method static. I will come back and post the code after I've polished it a bit.
You are correct olive. Caching works by caching the entire output of the Action (including all attributes) then returning the result to subsequent calls without actually calling any of your code.
Because of this you cannot cache and check authorization because by caching you are not going to call any of your code (including authorization). Therefore anything that is cached must be public.
I am converting an asp.net application into mvc3. Lets say I have a sign in page that requires https and every other page only needs http how can I redirect the signin to https and keep all the other pages on http?
Any suggestions would be great!
The RequireHttpsAttribute class may be what you want.
[RequireHttps]
public ActionResult SignIn() {
return View();
}
I like this solution because:
1. Cause you never have to touch it again in VS. The decorator by itself causes the browser to render the site in https even in debug.
If you implement it into a base controller and have all your controllers inherit from that controller, you know there is no slipping in the back do "So to speak".
#if !DEBUG
#define RELEASE
#endif namespace ProjectName.UI.Controllers {
using System.Web.Mvc;
#if RELEASE
[RequireHttps]
#endif
public abstract partial class ProjectNameBaseController : Controller
{
}
}
A co-worker of mine and I looked at this and believe we found a good solution. Here is what we came up with:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class HttpsRequiredAttribute : RequireHttpsAttribute
{
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
// Check to see if we're secure
string requirestr = ConfigurationManager.AppSettings["RequireHttps"];
bool require;
if (bool.TryParse(requirestr, out require) && require)
base.HandleNonHttpsRequest(filterContext);
}
}
This works by extending the RequireHttpsAttribute, and can be applied to a class or method, as indicated by the AttributeTargets. We then overrode the virtual function in RequireHttpsAttribute. What the method does is check the Web.config file for a key called "RequireHttps". If it cannot find it, or it's an invalid bool value (which is what the bool.tryparse checks), then it does not require Https. If it finds the value as true, then it requires HTTPS.
If you use this extension, you'll need to add a key in your Web.config called "RequireHttps," like so:
<add key="RequireHttps" value="true"/>
You can then turn off the requirement for Https by changing this variable for when you're debugging, or by changing your Web.config file to have the requirement wherever your site is deployed.
the issue is that after log in all your requests are going to be https even if you don't want it