Notify user of inadequate role ASP.NET MVC 4 simplemembership simplerole - c#

I am using ASP.NET MVC4 SimpleMembership and SimpleRoleProvider to determine authorization before exposing certain methods.
For example:
[Authorize(Roles = "Admin,Corporate")]
public ActionResult Edit(int id = 0)
{
//some code
return View(model)
}
If the user is not in the "Admin" or "Corporate" role (or their session has expired), they are correctly sent to the /Account/Login page.
However, one tester brought up a good point that once on this Login page, there is no hint as to why the user was sent here. If they simply aren't authorized to access the page they are trying to access, they keep logging in again and again and thinking the site is broken.
Ordinarily, I would add a property to the model or pass an optional parameter in the url with a message such as,
You do not have adequate permissions to access that page.
Please log in as an administrator.
or something to that effect. However, because the filter happens before they enter the method, where / how would I add the message?

Related

Secure way to Delete a record in ASP.Net MVC

I want to delete a product from my ASP.Net MVC 5 website. I want to know if adding [AntiForgeryToken] and [Authorize] is enough to secure the Delete operation?
View
<p>Delete: #Model.Name</p>
#using (Html.BeginForm("Delete", "ProductController", FormMethod.Post, new { ProductId = Model.ProductId }))
{
#Html.AntiForgeryToken()
<button type="submit">Delete</button>
}
Controller
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult Delete(long ProductId)
{
/* Do I need to check if the logged in User has permission to delete the product?
var product = ProductRepository.Get(Id);
if (product.Creator == User.Identity.GetUserId<long>())
{
ProductRepository.Delete(ProductId);
}
*/
// or, can I avoid the trip to DB and just delete the record?
ProductRepository.Delete(ProductId);
}
Scenario: A hacker registers on my website and create a valid account. Now the hacker views his own product and obviously he has an AntiForgeryToken. Can he now just change the ProductId in the browser and Post a request to delete someone else's Product?
Short answer. That is not enough.
Antiforgery tokens just say that the person making the original page request is the person making the update.
The base authorize attribute just verifies that the user is logged in.
What you are looking for is data security. There's an example of this on microsoft's own site.
What you've stated in your last paragraph, a hacker can sign up for an account create their own list of products and given what you show them in the url could guess legitimate other records to edit
Say you have a url
https://example.com/product/edit/13
what is preventing the user/hacker from guessing at
https://example.com/product/edit/12
or
https://example.com/product/edit/14
Without security at the data level that says what records a user can or can't access/update, you run into a situation where a malicious user could see or edit all kinds of information.
This is the exact scenario that FISERV found to expose other client information
from the article
Hermansen had signed up to get email alerts any time a new transaction
posted to his account, and he noticed the site assigned his alert a
specific “event number.” Working on a hunch that these event numbers
might be assigned sequentially and that other records might be
available if requested directly, Hermansen requested the same page
again but first edited the site’s code in his browser so that his
event number was decremented by one digit.

how to address this security breach in web api

I am using OAuth token based authentication in my web api based project.
If user is authenticated,an access token is generated as below.
{"access_token":"FFz_DC6zzEDD4mGOCk9172ijj3sGxCUWnk-tGanm9wGk76hMB8sHI8ImeWtdUKHHGNXv465ZSlbb-3fr_hr9DqUHc9Dm9OBI7XjJhdjdOpAGAGSFOpE0Y17LCEWTjCmEZotuf42Mpgl81ewoS7OlnH4b5w4PrtzJbIBpSAMoWObziL_U3mTkeFKvWrcWOfvlSCvhhBA9Dc3UTXv3HiHKWQk0T3-pvVy7ZuW2oac-IIuaq_GYaVkIZh7s9-YjX9KAL2Z9yfrPrVOQXZe_5OcNd7nS3tdT5odchEAiuWRYQ6t7Tfb2si4T6VdAe73OYefE0se1FeQsxbOiNaLyF8OwBqymEUzEG8tEHJ-cejVbhPw","token_type":"bearer","expires_in":1799,"as:client_id":"","user":"1","role":"1",".issued":"Thu, 16 Feb 2017 09:37:44 GMT",".expires":"Thu, 16 Feb 2017 10:07:44 GMT"}
Below is one of the api method.
[Authorize]
[HttpGet]
[Route("{userId}/{type}/")]
public IHttpResponse GetCustomerDetails(int userId, string type)
{
//my api stuff
}
I am using Postman for testing api. When I pass parameters as
http://localhost:50684/api/customer/1/gold
--along the access token in token in header--
It returns the desired json.
But if I use the same token & pass the customer id = 2,still it allows the access to the other customer(with id=2).
http://localhost:50684/api/customer/2/gold
--Access token in header--
It should NOT allow to access the resource to user with id=2 since the generated access token is valid for user with id =1.
How do I prevent this security breach?
Any help/suggestion highly appreciated.
Thanks
The problem is that you send the userId as a parameter which by itself is bad design.
The simple solution is to get the current user from the context instead
[Authorize]
[HttpGet]
[Route("{type}/")]
public IHttpResponse GetCustomerDetails(string type)
{
var user = RequestContext.Principal.Identity.Name;
//my api stuff
}
You can store user id and token in some storage(session, db).
And write own MVC authorization fileter like Authorize filter, which compare token with user id stored in storage.
Currently WebApi doesn't match the concept of user id and authenticated user's id. And it shouldn't, because the only thing you specify is the route of the controller's method with some parameter. You only require the user to be authenticated, by using "Authorize" attribute, but once the access is granted, no further validation is run. To make this method available to only specific subset of your users you could either write your own generic validation (i.e. in this case check against the claims of the user which can be accessed by "User" property within controller scope, or use some out-of-the-box external implementations of handling authentication.

Restrict access to the site for specific role

I have 3 roles: Registered Users, Approved Users, and Admins.
Access to the site is only available to Approved users and Admins.
To restrict anonymous access I've added a filter in FilterConfig as follows:
filters.Add(new System.Web.Mvc.AuthorizeAttribute());
Now, for registered users I want them to redirect to a landing page saying:
Please contact one of the administrators to approve you.
I'm not really sure what's the correct way to do that.
I can setup authorize attribute on each of the controllers, but I'm not sure if that's a good way.
Also, I'm not sure where I should specify default redirect action based on the role.
I know that I can specify default redirect action in RouteConfig.cs but not sure where to specify a role.
StanK is right that having [Authorize] attribute will redirect all users who are not logged-in to the login page. That's half of your dillema.
From there you need to alter your logon method to check if a newly logged-in user has the right role (e.g. ConfirmedUser). This is tricky because User.IsInRole("ConfirmedUser") will always be false in your logon method. This is because the User object is populated by the http object, which will not be re-populated until the next re-cycle. Luckily, you can use the Roles.IsUserInRole(userName, "ConfirmedUser") to check if the user has the right role.
So, within your logon method, after authenticating user, log the user out and re-direct them to an [AllowAnonymous] method which informs them that they are not yet confirmed.
if (Roles.IsUserInRole(userName, "ConfirmedUser")
{
FormsAuthentication.SignOut();
return RedirectToAction("WarningMsg", "Home");
}
You should be able to use the [Authorize] attributes for this.
Restricted pages will have their controller or action decorated with [Authorize(Roles="Approved User,Admin")], the 'landing page' for registered users would be [Authorize(Roles="Registered User,Approved User,Admin")] and the Logon action would have [AllowAnonymous].
If the user is not authorised, they would be re-directed to Account/Login. You would need to build some logic in this action that redirects "Registered Users" who are already logged in to your landing page. Others should just see the standard login page.
EDIT
The logic to redirect "Registered Users" from the login page to the landing page would look something like this
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
if (User.Identity.IsAuthenticated && Roles.IsUserInRole("Registered User"))
return RedirectToAction("LandingPage");
ViewBag.ReturnUrl = returnUrl;
return View();
}

Set proxy user in a GenericPrincipal, while keeping the old identity, using MVC

I have a site where I allow some users to proxy in as an other user. When they do, they should see the entire site as if they where the user they proxy in as. I do this by changing the current user object
internal static void SetProxyUser(int userID)
{
HttpContext.Current.User = GetGenericPrincipal(userID);
}
This code works fine for me.
On the site, to proxy in, the user selects a value in a dropdown that I render in my _layout file as such, so that it appears on all pages.
#Html.Action("SetProxyUsers", "Home")
The SetProxyUsers view looks like this:
#using (#Html.BeginForm("SetProxyUsers", "Home")) {
#Html.DropDownList("ddlProxyUser", (SelectList)ViewBag.ProxyUsers_SelectList, new { onchange = "this.form.submit();" })
}
The controller actions for this looks like this
[HttpGet]
public ActionResult SetProxyUsers()
{
ViewBag.ProxyUsers_SelectList = GetAvailableProxyUsers(originalUserID);
return PartialView();
}
[HttpPost]
public ActionResult SetProxyUsers(FormCollection formCollection)
{
int id = int.Parse(formCollection["ddlProxyUser"]);
RolesHelper.SetProxyUser(id);
ViewBag.ProxyUsers_SelectList = GetAvailableProxyUsers(originalUserID);
return Redirect(Request.UrlReferrer.ToString());
}
All this works (except for the originalUserID variable, which I put in here to symbolize what I want done next.
My problem is that the values in the dropdown list are based on the logged in user. So, when I change user using the proxy, I also change the values in the proxy dropdown list (to either disappear if the "new" user isn't allowed to proxy, or to show the "new" user's list of available proxy users).
I need to have this selectlist stay unchanged. How do I go about storing the id of the original user? I could store it in a session variable, but I don't want to mess with potential time out issues, so that's a last resort.
Please help, and let me know if there is anything unclear with the question.
Update
I didn't realize that the HttpContext is set for each post. I haven't really worked with this kind of stuff before and for some reason assumed I was setting the values for the entire session (stupid, I know). However, I'm using windows authentication. How can I change the user on a more permanent basis (as long as the browser is open)? I assume I can't use FormAuthentication cookies since I'm using windows as my authentication mode, right?
Instead of faking the authentication, why not make it real? On a site that I work on we let admins impersonate other users by setting the authentication cookie for the user to be impersonated. Then the original user id is stored in session so if they ever log out from the impersonated users account, they are actually automatically logged back in to their original account.
Edit:
Here's a code sample of how I do impersonation:
[Authorize] //I use a custom authorize attribute; just make sure this is secured to only certain users.
public ActionResult Impersonate(string email) {
var user = YourMembershipProvider.GetUser(email);
if (user != null) {
//Store the currently logged in username in session so they can be logged back in if they log out from impersonating the user.
UserService.SetImpersonateCache(WebsiteUser.Email, user.Email);
FormsAuthentication.SetAuthCookie(user.Email, false);
}
return new RedirectResult("~/");
}
Simple as that! It's been working great. The only tricky piece is storing the session data (which certainly isn't required, it was just a nice feature to offer to my users so they wouldn't have to log back in as themselves all the time). The session key that I am using is:
string.Format("Impersonation.{0}", username)
Where username is the name of the user being impersonated (the value for that session key is the username of the original/admin user). This is important because then when the log out occurs I can say, "Hey, are there any impersonation keys for you? Because if so, I am going to log you in as that user stored in session. If not, I'll just log you out".
Here's an example of the LogOff method:
[Authorize]
public ActionResult LogOff() {
//Get from session the name of the original user that was logged in and started impersonating the current user.
var originalLoggedInUser = UserService.GetImpersonateCache(WebsiteUser.Email);
if (string.IsNullOrEmpty(originalLoggedInUser)) {
FormsAuthentication.SignOut();
} else {
FormsAuthentication.SetAuthCookie(originalLoggedInUser, false);
}
return RedirectToAction("Index", "Home");
}
I used the mvc example in the comments on this article http://www.codeproject.com/Articles/43724/ASP-NET-Forms-authentication-user-impersonation to
It uses FormsAuthentication.SetAuthCookie() to just change the current authorized cookie and also store the impersonated user identity in a cookie. This way it can easily re-authenticate you back to your original user.
I got it working very quickly. Use it to allow admin to login as anyone else.

ASP.NET MVC Single Sign-on and Roles

I have basic Single Sign-On working across 2 MVC sites (call them SiteA and SiteB) using something along the lines of the following method:
http://forums.asp.net/p/1023838/2614630.aspx
They are on sub-domains of the same domain and share hash\encryption keys etc in web.config. I've modified the cookie so it is accessible to all Sites on the same domain. All of this seems to be working ok.
The sites are on separate servers without access to the same SQL database, so only SiteA actually holds the user login details. SiteB has a membership database, but with empty users.
This works fine for my required scenario which is:
1) User logs into SiteA
2) The application loads data from SiteA (by AJAX) and SiteB (by AJAX using JSONP)
I have the following LogOn Action on my AccountController for SiteA, which is where the "magic" happens:
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
//modify the Domain attribute of the cookie to the second level of domain
// Add roles
string[] roles = Roles.GetRolesForUser(model.UserName);
HttpCookie cookie = FormsAuthentication.GetAuthCookie(User.Identity.Name, false);
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
// Store roles inside the Forms cookie.
FormsAuthenticationTicket newticket = new FormsAuthenticationTicket(ticket.Version, model.UserName,
ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, String.Join("|", roles), ticket.CookiePath);
cookie.Value = FormsAuthentication.Encrypt(newticket);
cookie.HttpOnly = false;
cookie.Domain = ConfigurationManager.AppSettings["Level2DomainName"];
Response.Cookies.Remove(cookie.Name);
Response.AppendCookie(cookie);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
This does some stuff which I don't strictly need for the initial scenario, but relates to my question. It inserts the Roles list for the user on login to SiteA into the UserData of the authentication ticket. This is then "restored" on SiteB by the following in global.asax:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (Context.Request.IsAuthenticated)
{
FormsIdentity ident = (FormsIdentity) Context.User.Identity;
string[] arrRoles = ident.Ticket.UserData.Split(new[] {'|'});
Context.User = new System.Security.Principal.GenericPrincipal(ident, arrRoles);
}
}
All of the stuff above works until I add Roles into the mix. Things work fine if I only decorate my Controllers\Actions on SiteB with [Authorize] attributes. But as soon as I add [Authorize(roles="TestAdmin")] users can no longer access that Controller Action. Obviously I have added the user to the TestAdmin Role.
If I debug the global.asax code on SiteB, it looks ok as I leave the global.asax code, BUT then when I hit a break point in the controller itself the Controller.User and Controller.HttpContext.User is now a System.Web.Security.RolePrincipal without the roles set anymore.
So my question is: Does anybody have any idea how I can restore the roles on SiteB or another way to do this?
You already worked it out, but here we go:
make it work: turn off the role manager. Its not an odd behavior that asp.net is doing that, since you are explicitly telling it to use look for the user's roles with the configuration specified.
another way to do it: enable the role manager in both. Use the configuration to share the cookie as you are doing in your custom code. Based on your description, you shouldn't need to worry about it trying to get roles for the user, as long as you use a matching configuration for the authentication cookie
should you use Application_AuthorizeRequest to set the roles cookies? imho opinion earlier (Authenticate) is best, I have always done it that way and never ran into issues.
Since this seems to have stagnated I can partially answer this one with some additional findings. After debugging\testing this a bit more, it seems MVC2 is doing something odd after leaving Application_AuthenticateRequest but before entering my Controller. More details here:
http://forums.asp.net/t/1597075.aspx
A workaround is to use Application_AuthorizeRequest instead of Application_AuthenticateRequest.
EDIT: I believe I found the cause of my issues. On my MVC1 project I had roleManager disabled but my test MVC2 project had roleManager enabled. Once I enabled my roleManager in the MVC1 test project, the behaviour between MVC1 and MVC2 is the same. I also have a question to one of the MVC Microsoft team open about this, and will post back here if Application_AuthorizeRequest is the correct place to restore the Roles from Authentication ticket cookie...

Categories