I'd like to use caching in my application but the data I'm returning is specific to the logged in user. I can't use any of the out of the box caching rules when I need to vary by user.
Can someone point me in the right direction on creating a custom caching attribute. From the controller I can access the user from Thread.CurrentPrincipal.Identity; or a private controller member that I initialize in the controller constructor _user
Thank you.
You could use the VaryByCustom. In Global.asax override the GetVaryByCustomString method:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "IsLoggedIn")
{
if (context.Request.Cookies["anon"] != null)
{
if (context.Request.Cookies["anon"].Value == "false")
{
return "auth";
}
else
{
return "anon";
}
}
else
{
return "anon";
}
}
else
{
return base.GetVaryByCustomString(context, arg);
}
}
and then use the OutputCache attribute:
[OutputCache(CacheProfile = "MyProfile")]
public ActionResult Index()
{
return View();
}
and in web.config:
<caching>
<outputcachesettings>
<outputcacheprofiles>
<clear />
<add varybycustom="IsLoggedIn" varybyparam="*" duration="86400" name="MyProfile" />
</outputcacheprofiles>
</outputcachesettings>
</caching>
The Authorize Attribute has some interesting things going on regarding caching for authorized vs unauthorized users. You may be able to extract it's logic and modify it to cache per authorized user, instead of just per-"the user is authorized".
Check out this post:
Can someone explain this block of ASP.NET MVC code to me, please?
You should use OutputCache.VaryByCustom Property to specify custom vary string. And to use it, you should override method in your Global.asax
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if(arg.ToLower() == "currentuser")
{
//return UserName;
}
return base.GetVaryByCustomString(context, arg);
}
Related
Writing unit tests that require database access via my CustomMembershipProvider.
edit -
public class CustomMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
using (var usersContext = new UsersContext())
{
var requiredUser = usersContext.GetUser(username, password);
var userApproved = usersContext.GetUserMem(username);
if (userApproved == null) return false;
return (requiredUser != null && userApproved.IsApproved != false);
}
}
}
[TestFixture]
public class AccountControllerTest
{
[Test]
public void ShouldNotAcceptInvalidUser()
{
// OPTION1
Mock<IMembershipService> membership = new Mock<IMembershipService>();
//OPTION2
// Mock<AccountMembershipService> membership = new Mock<AccountMembershipService>();
membership.Setup(m => m.ValidateUser(It.IsAny<string>(), It.IsAny<string>()))
.Returns(false);
var logonModel = new LoginModel() { EmailorUserName = "connorgerv", Password = "pasdsword1" };
var controller = new AccountController(membership.Object);
// Act
var result = controller.Login(logonModel,"Index") as RedirectResult;
// Assert
Assert.That(result.Url, Is.EqualTo("Index"));
Assert.False(controller.ModelState.IsValid);
Assert.That(controller.ModelState[""],
Is.EqualTo("The user name or password provided is incorrect."));
}
[Test]
public void ExampleForMockingAccountMembershipService()
{
var validUserName = "connorgerv";
var validPassword = "passwordd1";
var stubService = new Mock<CustomMembershipProvider>();
bool val = false;
stubService.Setup(x => x.ValidateUser(validUserName, validPassword)).Returns(true);
Assert.IsTrue(stubService.Object.ValidateUser(validUserName, validPassword));
}
}
public class AccountController : Controller
{
public IMembershipService MembershipService { get; set; }
public AccountController(IMembershipService service){
MembershipService=service;
}
protected override void Initialize(RequestContext requestContext)
{
if (MembershipService == null) { MembershipService = new AccountMembershipService(); }
base.Initialize(requestContext);
}
public ActionResult Index()
{
return RedirectToAction("Profile");
}
public ActionResult Login()
{
if (User.Identity.IsAuthenticated)
{
//redirect to some other page
return RedirectToAction("Index", "Home");
}
return View();
}
//
// POST: /Account/Login
[HttpPost]
public ActionResult Login(LoginModel model, string ReturnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.EmailorUserName, model.Password))
{
SetupFormsAuthTicket(model.EmailorUserName, model.RememberMe);
if (Url.IsLocalUrl(ReturnUrl) && ReturnUrl.Length > 1 && ReturnUrl.StartsWith("/")
&& !ReturnUrl.StartsWith("//") && !ReturnUrl.StartsWith("/\\"))
{
return Redirect(ReturnUrl);
}
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
// If we got this far, something failed, redisplay form
return View(model);
}
}
public class AccountMembershipService : IMembershipService
{
private readonly MembershipProvider _provider;
public AccountMembershipService()
: this(null)
{
}
public AccountMembershipService(MembershipProvider provider)
{
_provider = provider ?? Membership.Provider;
}
public virtual bool ValidateUser(string userName, string password)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
return _provider.ValidateUser(userName, password);
}
}
Membership in web.config of main application
<membership defaultProvider="CustomMembershipProvider">
<providers>
<clear />
<add name="CustomMembershipProvider" type="QUBBasketballMVC.Infrastructure.CustomMembershipProvider" connectionStringName="UsersContext" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
</providers>
</membership>
public class CustomMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
using (var usersContext = new UsersContext())
{
var requiredUser = usersContext.GetUser(username, password);
var userApproved = usersContext.GetUserMem(username);
if (userApproved == null) return false;
return (requiredUser != null && userApproved.IsApproved != false);
}
}
}
What happens when I run ShouldNotAcceptInvalidUser() with Option1 uncommented I can see that MembershipService is a Mock<IMembershipService> in the AccountController but it never steps into MembershipService.ValidateUser on the login Action.
When I run with option2 uncommented the same thing happens except MembershipService is Mock<AccountMembershipService> in the accountcontroller and it hits the AccountMembership Contstructor with null parameters, which in turn sets is to SqlMembershipProvider as Membership.Provider is System.Web.Security.SqlMembershipProvider
Also ExampleForMockingAccountMembershipService() doesn't seem to hit the ValidateUsermethod at all in CustomMembershipProvider and always returns true.
Hopefully this is enough to see where i'm going wrong!! :/
Thanks for providing your code. I think I have a much better handle on what you're trying to do now.
For your ShouldNotAcceptInvalidUser test, you should definitely mock IMembershipService instead of AccountMembershipService (choose option 1 over option 2). Since your controller is your SUT, it should be the only "real" class in the test, in order to minimize the number of moving parts.
With option 1, there's no reason to expect that MembershipService.ValidateUser would step into any code. The MembershipService is a mock object - and you've explicitly told it to just always return false when that method is called. Based on the code here, and using option 1, I'd expect this test to pass.
In your other test, ExampleForMockingAccountMembershipService, you're mocking your SUT which is something you should not do. Your SUT should be the only "real" object in your test. That means all collaborating objects should be mocked, leaving the SUT to be the only object doing anything meaningful. (That way, if the test fails, you know for sure that it's because of a bug in the SUT.)
(Note: ValidateUser was always returning true here because you mocked the SUT, and explicitly told it to always return true. This is why it's never a good idea to mock your SUT - mocking changes the behavior that you're trying to test.)
Based on the code you provided, I'm guessing that the reason you mocked CustomMembershipProvider is because it doesn't fully implement its abstract base class MembershipService. If this is indeed the case, then you will need to implement the missing methods manually, instead of relying on the mocking framework to provide default implementations.
Here is what I believe you were intending this test to look like:
[Test]
public void ExampleForMockingAccountMembershipService()
{
var validUserName = "connorgerv";
var validPassword = "passwordd1";
var sut = new CustomMembershipProvider();
Assert.IsTrue(sut.ValidateUser(validUserName, validPassword));
}
Something to look out for here is the fact that CustomMembershipProvider instantiates one of its dependencies: UsersContext. In a unit test, since CustomMembershipProvider is your SUT, you'd want to mock all of its dependencies. In this situation, you could use dependency injection to pass an object responsible for creating this dependency (e.g., an IUsersContextFactory), and use a mock factory and context in your test.
If you don't want to go that route, then just be aware that your test could fail because of a bug in CustomMembershipProvider or a bug in UsersContext.
So, the general logic in your tests is sound; the problems mainly stem from confusion on the role of mock objects in your tests. It's kind of a tough concept to get at first, but here are some resources that helped me when I was learning this:
"Dependency Injection in .Net" by Mark Seemann
"Test Doubles" by Martin Fowler
I'm just reading about OutputCache, and I see how you can apply VaryByParam to change the cache based on parameters sent to the view, but I would like to change the cache based on both the parameters and the currently logged in user (using Asp.Nets default membership). I've been looking around, but I don't seem to be able to find a way to get this working.
Any suggestions on what I should try?
Use VaryByCustom. I implemented something like this:
[OutputCache(VaryByCustom="user")]
public ActionResult SomeAction()
{
return View();
}
and in Global.asax.cs
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "user")
{
return context.Request.User.Identity.IsAuthenticated ? context.Request.User.Identity.Name : string.Empty;
}
return base.GetVaryByCustomString(context, arg);
}
I'd like [Authorize] to redirect to loginUrl unless I'm also using a role, such as [Authorize (Roles="Admin")]. In that case, I want to simply display a page saying the user isn't authorized.
What should I do?
Here is the code from my modified implementation of AuthorizeAttribute; I named it SecurityAttribute. The only thing that I have changed is the OnAuthorization method, and I added an additional string property for the Url to redirect to an Unauthorized page:
// Set default Unauthorized Page Url here
private string _notifyUrl = "/Error/Unauthorized";
public string NotifyUrl {
get { return _notifyUrl; } set { _notifyUrl = value; }
}
public override void OnAuthorization(AuthorizationContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (AuthorizeCore(filterContext.HttpContext)) {
HttpCachePolicyBase cachePolicy =
filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null);
}
/// This code added to support custom Unauthorized pages.
else if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (NotifyUrl != null)
filterContext.Result = new RedirectResult(NotifyUrl);
else
// Redirect to Login page.
HandleUnauthorizedRequest(filterContext);
}
/// End of additional code
else
{
// Redirect to Login page.
HandleUnauthorizedRequest(filterContext);
}
}
You call it the same way as the original AuthorizeAttribute, except that there is an additional property to override the Unauthorized Page Url:
// Use custom Unauthorized page:
[Security (Roles="Admin, User", NotifyUrl="/UnauthorizedPage")]
// Use default Unauthorized page:
[Security (Roles="Admin, User")]
Extend the AuthorizeAttribute class and override HandleUnauthorizedRequest
public class RoleAuthorizeAttribute : AuthorizeAttribute
{
private string redirectUrl = "";
public RoleAuthorizeAttribute() : base()
{
}
public RoleAuthorizeAttribute(string redirectUrl) : base()
{
this.redirectUrl = redirectUrl;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
string authUrl = this.redirectUrl; //passed from attribute
//if null, get it from config
if (String.IsNullOrEmpty(authUrl))
authUrl = System.Web.Configuration.WebConfigurationManager.AppSettings["RolesAuthRedirectUrl"];
if (!String.IsNullOrEmpty(authUrl))
filterContext.HttpContext.Response.Redirect(authUrl);
}
//else do normal process
base.HandleUnauthorizedRequest(filterContext);
}
}
Usage
[RoleAuthorize(Roles = "Admin, Editor")]
public class AccountController : Controller
{
}
And make sure you add your AppSettings entry in the config
<appSettings>
<add key="RolesAuthRedirectUrl" value="http://mysite/myauthorizedpage" />
</appSettings>
The easiest way I've found is to extend and customize the AuthorizeAttribute so that it does something different (i.e., not set an HttpUnauthorizedResult) when the Role check fails. I've written an article about this on my blog that you might find useful. The article describes much what you are wanting, though it goes further and allows the user who "owns" the data to also have access to the action. I think it should be fairly easy to modify for your purposes -- you'd just need to remove the "or owner" part.
What is the best way to protect certain areas of your web application in asp .net mvc. I know we can put [Authorization] attribute at each action, but this seems very tedious since you have to put it all over the place. I'm using membership provider and trying the way I used to do in postback model by setting this protection based on the folder. I use web.config <location> section to protect some folders. I tried this in mvc, it seems to be working, but most of tutorial uses the [Authorization] way.
Which one is the better method?
I'd highly recommend against putting it in the web.config. Actually, so do Conery, Hanselman, Haack, and Guthrie -- though not highly (p223 of Professional ASP.NET MVC 1.0)
Routes are subject to change, especially in MVC. With the WebForm model, routes are physically represented on the file system so you didn't really have to worry about it. In MVC, routes are "dynamic" for lack of a better term.
You could end up with multiple routes mapping to one controller causing a maintenance pain in the web.config. Worse, you could inadvertently have a route invoke a controller accidentally or forget to update the web.config after adding/modifying routes and leave yourself open.
If, however, you secure your controller instead of the actual route, then you don't need to worry about keeping the web.config in sync with the goings-on of the controllers and changing routes.
Just my 2 cents.
One possible solution is to create a "protected controller" and use it as a base class for all the areas of your application that you want to protect
[Authorize]
public class ProtectedBaseController : Controller {
}
public class AdminController : ProtectedBaseController {
...
}
public class Admin2Controller : ProtectedBaseController {
...
}
put [Authorisation] at the top of the controller class. that will lock down the entire controllers actions.
You can put [Authorize] to every contoller you need to secure.
You can add filter GlobalFilters.Add(new AuthorizeAttribute()); in your Startup.cs (or Global.asax) and put [AllowAnonymus] attribute to any controller or action you allow to non-registered users.
If you chose to put [Authorize] to every secure contoller you need to be sure that any controller added by you or anyone other in team will be secure. For this requirement I use such test:
[Fact]
public void AllAuth()
{
var asm = Assembly.GetAssembly(typeof (HomeController));
foreach (var type in asm.GetTypes())
{
if (typeof(Controller).IsAssignableFrom(type))
{
var attrs = type.GetCustomAttributes(typeof (AuthorizeAttribute));
Assert.True(attrs.Any());
}
}
}
I think this way is better than a creating ProtectedContoller, because it make no guarantee that you system have all controllers secure. Also this way doesn't use inheritance, which make project heavier.
Authorization is one way to secure your application; is to apply the attribute to each controller.
Another way is to use the new AllowAnonymous attribute on the login and register actions.
Making secure decisions based on the current area is a Very Bad Thing and will open your application to vulnerabilities.
Code you can get here
As ASP.NET MVC 4 includes the new AllowAnonymous attribute, so you no more need to write that code.
After setting the AuthorizeAttribute globally in global.asax and then whitelisting will be sufficient.
This methods you want to opt out of authorization is considered a best practice in securing your action methods. Thanks.
[Area("AdminPanel")]
public class TestimonialsController : Controller
{
private AppDbContext _context;
private IWebHostEnvironment _env;
public TestimonialsController(AppDbContext context, IWebHostEnvironment env)
{
_context = context;
_env = env;
}
public IActionResult Index()
{
return View(_context.Testimonials);
}
// GET: AdminPanel/Testimonials/Create
public IActionResult Create()
{
return View();
}
// POST: AdminPanel/Testimonials/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Testimonial testimonial)
{
if (!ModelState.IsValid)
{
return View();
}
if (!testimonial.Photo.CheckFileType("image/"))
{
return View();
}
if (!testimonial.Photo.CheckFileSize(200))
{
return View();
}
testimonial.Image = await testimonial.Photo.SaveFileAsync(_env.WebRootPath, "images");
await _context.Testimonials.AddAsync(testimonial);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: AdminPanel/Testimonials/Edit/5
public async Task<IActionResult> Update(int? id)
{
if (id == null)
{
return BadRequest();
}
var testimonial = await _context.Testimonials.FindAsync(id);
if (testimonial == null)
{
return NotFound();
}
return View(testimonial);
}
// POST: AdminPanel/Testimonials/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update(int? id, Testimonial newtestimonial)
{
if (id==null)
{
return BadRequest();
}
var oldtestimonial = _context.Testimonials.Find(id);
if (oldtestimonial == null)
{
return NotFound();
}
if (!ModelState.IsValid)
{
return View();
}
if (!newtestimonial.Photo.CheckFileType("image/"))
{
return View();
}
if (!newtestimonial.Photo.CheckFileSize(200))
{
return View();
}
var path = Helper.GetPath(_env.WebRootPath, "images", oldtestimonial.Image);
if (System.IO.File.Exists(path))
{
System.IO.File.Delete(path);
}
newtestimonial.Image = await newtestimonial.Photo.SaveFileAsync(_env.WebRootPath, "images");
oldtestimonial.Image = newtestimonial.Image;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
public async Task<IActionResult> Delete(int id)
{
if (id == null)
{
return BadRequest();
}
var testimonial = _context.Testimonials.Find(id);
if (testimonial == null)
{
return NotFound();
}
var path = Helper.GetPath(_env.WebRootPath, "images", testimonial.Image);
if (System.IO.File.Exists(path))
{
System.IO.File.Delete(path);
}
_context.Testimonials.Remove(testimonial);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
}
Essentially I want to show a friendly message when someone is not part of a role listed in my attribute. Currently my application just spits the user back to the log in screen. I've read a few posts that talk about creating a custom attribute that just extends [AuthorizeAttribute], but I'm thinking there's got to be something out of the box to do this?
can someone please point me in the right direction of where I need to look to not have it send the user to the log in form, but rather just shoot them a "not authorized" message?
I might be a little late in adding my $0.02, but when you create your CustomAuthorizationAttribue, you can use the AuthorizationContext.Result property to dictate where the AuthorizeAttribute.HandleUnauthorizedRequest method directs the user.
Here is a very simple example that allows you to specify the URL where a user should be sent after a failed authorization:
public class Authorize2Attribute : AuthorizeAttribute
{
// Properties
public String RedirectResultUrl { get; set; }
// Constructors
public Authorize2Attribute()
: base()
{
}
// Overrides
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (String.IsNullOrEmpty(RedirectResultUrl))
base.HandleUnauthorizedRequest(filterContext);
else
filterContext.Result = new RedirectResult(RedirectResultUrl);
}
}
And if I wanted to redirect the user to /Error/Unauthorized as suggested in a previous post:
[Authorize2(Roles = "AuthorizedUsers", RedirectResultUrl = "/Error/Unauthorized")]
public ActionResult RestrictedAction()
{
// TODO: ...
}
I ran into this issue a few days ago and the solution is a bit detailed but here are the important bits. In AuthorizeAttribute the OnAuthorization method returns a HttpUnauthorizedResult when authorization fails which makes returning a custom result a bit difficult.
What I ended up doing was to create a CustomAuthorizeAttribute class and override the OnAuthorization method to throw an exception instead. I can then catch that exception with a custom error handler and display a customized error page instead of returning a 401 (Unauthorized).
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public virtual void OnAuthorization(AuthorizationContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (AuthorizeCore(filterContext.HttpContext)) {
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else {
// auth failed, redirect to login page
// filterContext.Result = new HttpUnauthorizedResult();
throw new HttpException ((int)HttpStatusCode.Unauthorized, "Unauthorized");
}
}
}
then in your web.config you can set custom handlers for specific errors:
<customErrors mode="On" defaultRedirect="~/Error">
<error statusCode="401" redirect="~/Error/Unauthorized" />
<error statusCode="404" redirect="~/Error/NotFound" />
</customErrors>
and then implement your own ErrorController to serve up custom pages.
On IIS7 you need to look into setting Response.TrySkipIisCustomErrors = true; to enable your custom errors.
If simplicity or total control of the logic is what you want you can call this in your action method:
User.IsInRole("NameOfRole");
It returns a bool and you can do the rest of your logic depending on that result.
Another one that I've used in some cases is:
System.Web.Security.Roles.GetRolesForUser();
I think that returns a string[] but don't quote me on that.
EDIT:
An example always helps...
public ActionResult AddUser()
{
if(User.IsInRoles("SuperUser")
{
return View("AddUser");
}
else
{
return View("SorryWrongRole");
}
}
As long as your return type is "ActionResult" you could return any of the accepted return types (ViewResult, PartialViewResult, RedirectResult, JsonResult...)
Very similar to crazyarabian, but I only redirect to my string if the user is actually authenticated. This allows the attribute to redirect to the standard logon page if they are not currently logged in, but to another page if they don't have permissions to access the url.
public class EnhancedAuthorizeAttribute : AuthorizeAttribute
{
public string UnauthorizedUrl { get; set; }
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var redirectUrl = UnauthorizedUrl;
if (filterContext.HttpContext.User.Identity.IsAuthenticated && !string.IsNullOrWhiteSpace(redirectUrl))
{
filterContext.Result = new RedirectResult(redirectUrl);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
The out-of-the-box behavior is that the [Authorize] attribute returns an HTTP 401. The FormsAuthenticationModule (which is loaded by default) intercepts this 401 and redirects the user to the login page. Take a look at System.Web.Security.FormsAuthenticationModule::OnLeave in Reflector to see what I mean.
If you want the AuthorizeAttribute to do something other than return HTTP 401, you'll have to override the AuthorizeAttribute::HandleUnauthorizedRequest method and perform your custom logic in there. Alternatively, just change this part of ~\Web.config:
<forms loginUrl="~/Account/LogOn" timeout="2880" />
And make it point to a different URL, like ~/AccessDenied.