My MVC application consists of Admin and normal users. Admin users can register as different customers using the system, and they can add their respective users to the system.
The controllers already have Authorize filters on certain controllers to prevent unauthorised access to certain pages/json.
However, say I have an admin user logged in, what prevents them from inspecting the Json posts/gets in the JavaScript and manually calling a 'get' with the Id of the specific data to be viewed? e.g. /Users/1
Testing my application, I could post an AJAX get, to retrieve the details of another user in the system which was not under the management of the current authenticated user.
I could write access check methods whenever a service method is called to see if the user can view the data. Are they any good ways of solving this problem without littering the application with access check methods all over the place?
e.g. Current Implementation
public class PeopleController : ApplicationController
{
public ActionResult GetMemberDetails(int memberId)
{
var member = _peopleService.GetMemberById(memberId);
return Json(member, JsonRequestBehavior.AllowGet);
}
}
public class PeopleService : IPeopleService
{
public MemberModel GetMemberById(int memberId)
{
// Void function which throws Unauthorised exception
MemberAccessCheck(memberId);
var member = Mapper.Map<Member, MemberModel>(_memberRepository.GetById(memberId));
return member;
}
}
The MemberAccessCheck function is called lots of times in my service.
You're going to have to write these checks inside your controller or service. I dont think you should use a custom attributefilter for this behaviour because you are basically filtering data from a service layer standpoint. Nor is Auhorize attribute suitable for this featire also.
What I suggest is that you have a service method that accepts the current userId from the controller (take the User.Identity) and send it to the service to get the list of user's or single user that they can view/modify. So its not necessarily littering with access checks, but it would be how your service operates (a business rule).
Eg of Service Method:
User GetAdminUser(int userId, int adminId);
List<User> GetAdminUsers(int adminId);
Or, just overload your previous service
User GetUser(int userId);
User GetUser(int userId, int adminId);
You should consider placing all of your Admin functions in an Area. If you do, then you can use the Route for the Area to implement a RouteConstraint, which can inspect the id submitted in the AJAX request to determine whther it is within the requesting user's scope. Your hypothetical Route in the AreaRegistration.cs file for the Area would look like the following:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { id = new IsActionAuthorized() }
);
}
Then, your RouteConstraint would look something like this:
public class IsActionAuthorized : IRouteConstraint
{
public IsActionAuthorized()
{ }
private MyEntities _db = new MyEntities();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if no id was submitted
if (String.IsNullOrEmpty(values[parameterName]))
return true;
// return true if action is authorized
var requestId = Convert.ToInt32(values[parameterName]);
var authorized = false;
// code to check against requester's id, to determine
// if requestedId is within requester's authority
// set authorized to true only if the request is authorized for this requester
return authorized;
}
}
This gives you a single entry point, where you can perform the necessary gatekeeping tests. If the tests fail, the request will fail routing and return a 404 error.
However, say I have an admin user logged in, what prevents them from
inspecting the Json posts/gets in the JavaScript and manually calling
a 'get' with the Id of the specific data to be viewed? e.g. /Users/1
Nothing, really.
You can use HTTPS for those requests so at least the data would be encrypted.
I could write access check methods whenever a service method is
called to see if the user can view the data.
You probably will need to do this. On a web application you cannot trust that the request that comes to you is valid.
This code tends to be very application specific so there aren't many "frameworks" that handle it for you. Your best bet would be to write this code in a way that can easily be hooked into your application everywhere you need it.
Related
I have a provider hosted sharepoint add-in which uses a database at back end. This database has some roles like Employer and Employee in DB marked with a number. For example 1 for Employer and 2 for Employee and corresponding to every row a sharepoint email address.
In my add-in, I want to mark all my actions with [Authorize(Role="Employer")] attribute but I am not sure how to proceed? If I create a custom filter then, does that mean on every action, I need to call SP to get current logged in user email address -> query DB using it -> find role -> proceed or give access denied. It will consume lots of time as there is already a SPContextFilter on every action.
I was initially saving current user details in a cookie (HttpOnly set to true) but got to know that anyone can edit it using browser extension and impersonate users.
I am pretty new to MVC so any help is appreciated.
I don't see any other way around, you will have to make a DB call for the first new request and for the subsequent requests save the user and role details in some persistent object. Consider using ViewState objects and maybe check for null before proceeding to make a database call and populating the ViewState again.
Always avoid saving user details in cookie. Check the user access with the db.
Create User Access Table to check whether the user has access or not
Use Action Filter, which will execute before the Action execute.
In controller
[RoleAccess]
public class AccountController : Controller
{
public ActionResult Index()
{
your code
}
}
The [RoleAccess] is a Action filter function.
In FilterConfig.cs
public class RoleAccessAttribute : ActionFilterAttribute
{
private ApplicationDbContext db = new ApplicationDbContext();
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
var userID = HttpContext.Current.User.Identity.GetUserId();
if (access not accepted)
{
//return the user to access denied page
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary {
{"controller","Error"},
{"action","Error403"}
});
}
}
}
If the access is accepted then the user is authorized to access the requested Action
hope this helps
I have been looking for answer to this question for a few days now, but I have not found any success. I would post the links, but it would probably take up the entire page.
So here is what I have...
I have an MVC application, which uses the WC-Federation protocol. I have been able to configure the application, so that it authenticates the users, and returns the claims from ADFS. This works perfect. I can also extract all the claims with no issues. But I am doing this within one of the actions in the controller.
And here is what I want to do...
I want to use ADFS to authenticate the user, but I want to use my own internal roles to authorize the user to have access to specific controllers (e.g. [Authorize(Roles = "CoolRole")]). I want to be able to do this, because I already have a Web API that uses OAuth 2.0, with a backend SQL Server database to manage users and roles (internal and external user.) I now want a secure portal that will allow internal users to access the data with a single-sign-on experience. Looking at the Controller model, I noticed there are some properties associated with the authentication process (OnAuthentication, OnAuthenticationChallenge) and one for the authorization process (OnAuthorization.)
I don't necessarily need the code, but I feel like I've hit a brick all, and I need to be pointed in the right direction.
UPDATE
I tried this:
protected override void OnAuthorization(
System.Web.Mvc.AuthorizationContext filterContext)
{
//Private class to create a new IPrincipal based on my AppUserMgr
var user = _setCurrentUser(
(ClaimsIdentity)filterContext.HttpContext.User.Identity);
filterContext.HttpContext.User = user;
base.OnAuthorization(filterContext);
}
This returned a 401 (Unauthorized) response.
and...
protected override void OnAuthentication(
System.Web.Mvc.Filters.AuthenticationContext filterContext)
{
//Private class to create a new IPrincipal based on my AppUserMgr
var user = _setCurrentUser(
(ClaimsIdentity)filterContext.HttpContext.User.Identity);
filterContext.Principal = user;
base.OnAuthorization(filterContext);
}
This just calls the STS numerous times, before it fails. I even tried swapping after the assignment to after the base is called in both. No luck.
Prior to the previous ones, I also tried to add an AuthorizeFilter to the control, but that didn't help:
http://pratapreddypilaka.blogspot.in/2012/03/custom-filters-in-mvc-authorization.html
I found this link: http://brockallen.com/2013/01/17/adding-custom-roles-to-windows-roles-in-asp-net-using-claims/
From there, I guessed my way through
Here is the basics of what I did:
I ended up overriding the OnAuthentication method of the Controller, but still made sure to call the base. I did this from within an extended class. Here is the concept:
public class AdfsController : Controller
{
//Some code for adding the AppUserManager (used Unity)
protected override void OnAuthentication(
System.Web.Mvc.Filters.AuthenticationContext filterContext)
{
base.OnAuthentication(filterContext);
//Private method to set the Principal
_setCurrentUser(filterContext.Principal);
}
private void _setCurrentUser(IPrincipal principal)
{
//Put code to find to use your ApplicationUserManager or
//dbContext. roles is a string array
foreach(var role in roles)
{
((ClaimsIdentity)((ClaimsPrincipal)principal).Identity)
.AddClaim(new Claim(ClaimTypes.Role, role));
}
}
}
In the Controller, you can now add the follow:
public class HomeController : AdfsController
{
//I used a magic string for demo, but store these in my globals class
[Authorize(Roles = "CoolRole")]
public ActionResult Index()
{
return View();
}
}
I tested this by checking a role assigned to the current user, and that worked! Then I changed the role to something like "reject", which the user was not assigned; and I received a 401 Unauthorized.
ADFS is the authentication/token service in Azure. to enable the Roles Based Authentication, you can use Azure RBAC (Role Based Access Controll) service to basically Augment the claims that you get back from the ADFS and add the roles that you get back from RBAC to the token, and use the same token in your API so lock down or secure the backend with that augmented token...
here is the reference for RBAC:
http://azure.microsoft.com/en-in/documentation/articles/role-based-access-control-configure/
I just wanted to gauge opinions on how I should approach this problem and ultimately looking for a quick win (wrong way to think about things nut time pressures mean I have to think and act quickly!
I've been given a website that has a bit of an issue.
I login using standard forms authentication as User1234 and my url is as follows:
www.mywebsite.co.uk/1234/Contact.
This will take me to User1234's details.
You can put two and two together and correctly assume that 1234 is a user id of some sort.
Once authenticated, I can access the views with [Authorize] attribute present, any anonymous/unathenticated users get redirected.
However, once logged in as User1234, I can then tinker with the url like so:
www.mywebsite.co.uk/1235/Contact.
So I am authenticated as User1234 but can see User1235's data. This is BAD for obvious reasons.
When I log in, I actively set the login ID in session so in theory, I could do a check whenever a user hits an ActionResult, I could cross check the ID present in the URL against the session login ID. However, it is a rather project with lots of action results and as such, I'm reluctant to spend my Saturday afternoon adding something to each and every ActionResult.
Is there an event in the global.asax I could use that is hit on each ActionResult request where I can compare Session login ID with url ID?
Alternatively, can anyone offer some suggestions about how I can achieve this or restrict URL tampering?
You can try and do a base controller
public class BaseController : Controller
{
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Do your stuff here
base.OnActionExecuted(filterContext);
}
}
I assume that you don't want to change your URL routes, as you could retrieve the user id also from the session. A quick solution would be to use an ActionFilter which you can place on the affected controllers or action methods:
public class VerifyUserIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var sessionUserId = filterContext.HttpContext.Session["UserId"];
var routeUserId = filterContext.RouteData.Values["UserId"];
if (routeUserId != null && sessionUserId == routeUserId)
filterContext.Result = new RedirectResult("<<url to redirect to>>");
}
}
I don't understand why the URL contains a data entry point. This appears to be a design flaw. I would remove all code that uses a URL parameter and instead make sure the controller looks up what the ID is based on the logged in user.
I am implementing a collaborative web gallery, and I have a few roles for each user:
Admin
DeleteImage
DeleteOwnImage
etc..
For any controller-action, we can apply [Authorize] tag to them plus which roles we want to allow, right? It is fine for Admin/DeleteImage since these two are global; but my question is, like DeleteOwnImage is kind of contextual, in order to determine whether it is valid, we need:
To know what image it is trying to delete (from request)
Retrieve the owner of that image (from service or repository)
Compare current user = that owner
Obviously [Authorize] is not enough to do so, but is it possible to do that on custom ActionFilters? Any hint?
Yes, this is possible with a custom action filter. You can extend from AuthorizeAttribute, the most basic implementation being something like:
public class OwnImageAuthorizeAttribute : AuthorizeAttribute {
public string ImageIdKey { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext) {
bool authorized = false;
// Get the current user
var currentUser = ...;
// Get the image ID, whether it is in the route or querystring
int imageId
if(int.TryParse(httpContext.RouteData.Values(ImageIdKey), out imageId)) {
// From querystring: httpContext.Request.Querystring[ImageIdKey]
// Authorize the user
authorized = YourMethodToCheckIfUserIsOwner(currentUser, imageId);
}
return authorized;
}
Then, decorate your method:
[OwnImageAuthorize(ImageIdKey = "imageId")]
public ActionResult MyAction() { }
You can find some more details here.
You can easily add something like this in an ActionFilter, Just add an action filter with OnActionExecuting implemented.
Although, depending on your DB schema this could be achieved on the DB level with your query. You could just delete when owner equals recieved recieved id. (I mean, inside the action method and not in the filter)
EDIT:
If you're using some kind of IOC container for the repositories, you should look around for the new IOC features in MVC3 (if you're using MVC3) to inject dependencies into your action filters.
http://bradwilson.typepad.com/blog/2010/07/service-location-pt4-filters.html
EDIT2:
BTW, I myself don't really like doing too much business logic in ActionFilters, especially involving calls to the DB. Even more when it's something very specific that'll be used for one action.
Basically, I log into my website using OpenId, very similar to what I am assuming SO does. When I get the information back, I throw it into a database and create my "Registered User". I set my AuthCookie:
FormsAuthentication.SetAuthCookie(user.Profile.MyProfile.DisplayName, false);
Then I can use this for the User Name. However, I would like to pass in the entire object instead of just the string for display name. So my question is:
How does SO do it?
Do they extend/override the SetAuthCookie(string, bool) method to accept the User object, i.e. SetAuthCookie(User(object), bool).
What is the best way to persist a User object so that it is available to my UserControl on every single page of my Web Application?
Thanks in advance!
You can achieve this behavior by implementing your custom Membership Provider, or extending an existing one. The provider stores user information based on a key (or just by user name) and provides access to the MembershipUser class, which you can extend however you wish. So when you call FormsAuthentication.SetAuthCookie(...), you basically set the user key, which can be accessed be the provider.
When you call Membership.GetUser(), the membership infrastructure will invoke the underlying provider and call its GetUser(...) method providing it with a key of the current user. Thus you will receive the current user object.
Jeff,
As I said in a comment to your question above, you must use the ClaimedIdentifier for the username -- that is, the first parameter to SetAuthCookie. There is a huge security reason for this. Feel free to start a thread on dotnetopenid#googlegroups.com if you'd like to understand more about the reasons.
Now regarding your question about an entire user object... if you wanted to send that down as a cookie, you'd have to serialize your user object as a string, then you'd HAVE TO sign it in some way to protect against user tampering. You might also want to encrypt it. Blah blah, it's a lot of work, and you'd end up with a large cookie going back and forth with every web request which you don't want.
What I do on my apps to solve the problem you state is add a static property to my Global.asax.cs file called CurrentUser. Like this:
public static User CurrentUser {
get {
User user = HttpContext.Current.Items["CurrentUser"] as User;
if (user == null && HttpContext.Current.User.Identity.IsAuthenticated) {
user = Database.LookupUserByClaimedIdentifier(HttpContext.Current.User.Identity.Name);
HttpContext.Current.Items["CurrentUser"] = user;
}
return user;
}
}
Notice I cache the result in the HttpContext.Current.Items dictionary, which is specific to a single HTTP request, and keeps the user fetch down to a single hit -- and only fetches it the first time if a page actually wants the CurrentUser information.
So a page can easily get current logged in user information like this:
User user = Global.CurrentUser;
if (user != null) { // unnecessary check if this is a page that users must be authenticated to access
int age = user.Age; // whatever you need here
}
One way is to inject into your controller a class that is responsible for retrieving information for the current logged in user. Here is how I did it. I created a class called WebUserSession which implements an interface called IUserSession. Then I just use dependency injection to inject it into the controller when the controller instance is created. I implemented a method on my interface called, GetCurrentUser which will return a User object that I can then use in my actions if needed, by passing it to the view.
using System.Security.Principal;
using System.Web;
public interface IUserSession
{
User GetCurrentUser();
}
public class WebUserSession : IUserSession
{
public User GetCurrentUser()
{
IIdentity identity = HttpContext.Current.User.Identity;
if (!identity.IsAuthenticated)
{
return null;
}
User currentUser = // logic to grab user by identity.Name;
return currentUser;
}
}
public class SomeController : Controller
{
private readonly IUserSession _userSession;
public SomeController(IUserSession userSession)
{
_userSession = userSession;
}
public ActionResult Index()
{
User user = _userSession.GetCurrentUser();
return View(user);
}
}
As you can see, you will now have access to retrieve the user if needed. Of course you can change the GetCurrentUser method to first look into the session or some other means if you want to, so you're not going to the database all the time.