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

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.

Related

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

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?

How can I get the Role for a user after Login?

Here is my sample code for logging in:
if (WebSecurity.Login(playerModel.WorkerID, playerModel.Password))
{
if (Roles.IsUserInRole(User.Identity.Name, "Administrator"))
{
// display the admin panel
ViewBag.Message = "Admin Panel";
return View("Index");
}
else
{
ViewBag.ButtonText = "Log In";
ViewBag.Message = "You are not an Admin user";
return View();
}
}
For some reason, the first time I log in, the user is not in "Administrator" even though they are in the correct Role in the table. I have also tried using this:
Roles.GetRolesForUser();
That returns an empty array. I'm assuming there is a limitation to do with the user logging in, a post-back occurring and then being able to check the role. But I'm hoping there is a workaround (or I have something wrong).
After a post-back both the Roles.IsUserInRole and Roles.GetRolesForUSer methods work correctly.
you could try using
HttpContext.Current.User.IsInRole("role")
I'm not 100% sure on when the Roles security class gets updated after a login - but I bets its not until the forward page gets called and a whole new request is generated.
The solution was to redirect to another page and check there. The issue is that the cookie isn't read back in with the role information until you have another postback (read the cookie you just wrote).

Good way of storing UserProfile information and preventing database call on every page load

I am trying to create a way to store user's profile information in either session or cache and be able to have that information on hand every time the page loads to prevent calling the database every time we load a page. I want to be able to put some of these values in a shared layout (so i can put the user's name in the upper right hand corner etc.). At the top of my master page layout I have the following:
#{
Domain.Entities.UserProfile user = (Domain.Entities.UserProfile)HttpContext.Current.Session[Membership.GetUser().ProviderUserKey.ToString()];
}
I use values like user.FirstName in my master page. When a user logs in I set the session. Is there a better way of doing this? If I am logged out and I just navigate to a member only page, the [Authorize] filter is bypassed and I get an error Object reference not set to an instance of an object. because my user profile is not stored in session yet. I do not store any sensitive information in the session like a UserId or anything, just the users name, profile image url etc. I feel there may be a better way of doing such a task.
You can store your user information in the authentication cookie. Keep in mind that the cookie will be sent back and forth from the client with every request. So if there's a lot of information to be stored maybe that's not the right way.
Check the step 4 of this document that explains how to store additional user data in the cookie http://www.asp.net/web-forms/tutorials/security/introduction/forms-authentication-configuration-and-advanced-topics-cs
Then in your master page, you can have something like this
#{
if (User.Identity.IsAuthenticated)
{
//Your code goes here, no way user data is null.
}
You can create a static class as an interface to get the user properties, which will be stored in the session state.
public class UserData{
public static Domain.Entities.UserProfile GetUser(){
string username;
try{
username = Membership.GetUser().ProviderUserKey.ToString();
}catch(Exception e){
// just in case
throw new AuthenticationException("The user isn't authenticated!");
}
var user = HttpContext.Current.Session[username] as Domain.Entities.UserProfile;
if(user == null){
user = // get data from database
HttpContext.Current.Session[username] = user;
}
return user;
}
}
Then, in your methods you can use it like this:
if (User.Identity.IsAuthenticated)
{
UserProfile user = UserData.GetUser();
//Your code goes here, no way user data is null.
}
Or in controllers:
[Authorize]
public ActionResult Index(){
UserProfile user = UserData.GetUser();
return //stuff
}
Note: I made this without being able to test, but I hope you get the point :)

Deleted User logged in even after deleting on the other broswer

var query = from p in AdminModelContext.Users
where p.UserName == model.UserName && p.Password == encryptPassword
&& p.IsDeleted == false
select p;
IList<Users> userList = query.ToList();
if (userList.Count() > 0)
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (CheckUrl(returnUrl))
{
return Redirect(returnUrl);
}
SetRoleForUser(userList[0].RolesId);
LoggerService.Info(string.Format("Login Successful for the user : {0}",
model.UserName));
return RedirectToAction("Index", "Home");
}
I am using the following code to Login through my website. The problem that I am facing is when I logged in with a user on a specific browser and at the same time login with a different user on a different browser, and then I delete the user(logged in on the other browser). Still I am able to navigate through the pages with the deleted user logged in.
I am not finding a fair solution to put authentication logic on every page. My website is in MVC model and using Form based authentication.
Please suggest how can I put the logged in user session validation and achieve this.
None of the answers so far actually acknowledge the question.
Lets look at the control flow:
User A enters log in page, supplies valid credentials
User A is issued Ticket A.
User B enters site, supplies valid credentials.
User B is issued Ticket B.
User B then revokes User A's access CREDENTIALS.
At this point nothing happens to Ticket A. Because the Ticket is independent on the credentials. When Ticket A expires they will then be required to present their credentials and it will fail login.
So what you've noticed that kicking a live user out of your site is actually pretty hard. As you've realized the ONLY solution is to have authentication logic on EVERY request. That unfortunately is really heavy.
In the login system I built I handled this aspect by creating 2 tickets, 1 ticket that's stored in the Forms Auth ticket as normal that has a big duration, and a ticket that's stored in HttpRuntime.Cache, I set the cache expiration to 15 minutes on this ticket.
On every page request I check to see whether a user has a ticket in the cache (based off their Forms Auth ticket information), at this point if they have no ticket I do a user data refresh and poll the user database. If the user has become suspended or deleted they will be logged out at then.
Using this method I know that my site can disable a user and within 15 minutes that user will be barred from the site. If I want them immediately barred I can just cycle the app config to clear the cache and FORCE it to happen.
Normally if you have the [Authorize] attribute defined for an Controller or an Action the Authentication is checked on every post back.
The build in MembershipProvider handles all that for you. But it seems you are using your own user Database. Then you have to implement your own MembershipProvider, IPrincipal and MembershipUser and have this added to your Web.config replacing the default one.
More you'll find here how to implement your own MembershipProvider: http://msdn.microsoft.com/en-us/library/f1kyba5e.aspx
My suggestion is to create an empty MVC project and have a look at the default authentication mechanism. And if your building a new Application with a new Database, try to use the default authentication.
Your validateUser function in your own MembershipProvider could look like this.
public override bool ValidateUser(string username, string password)
{
bool isValid = false;
bool isApproved = false;
string pwd = "";
using (AdminModelContext db = new AdminModelContext())
{
var user = db.Users.FirstOrDefault(u => u.UserName == username);
if (user != null)
{
pwd = user.Password;
isApproved = user.IsApproved;
if (CheckPassword(password, pwd))
{
if (isApproved)
{
isValid = true;
user.LastLoginDate = DateTime.Now;
user.LastActivityDate = DateTime.Now;
try
{
db.SubmitChanges();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
else
{
UpdateFailureCount(username, "password");
}
}
}
return isValid;
}
I see the problem now. I dont know how this works in MVC, but by using Authenticate_Request, you can validate if the user is still valid. The business logic may also double check if the user is still valid. But as far as I know, there is no way of iterating all the open sessions and killing the requierd ones, even in that case, the authorization cookie should be double checked on Session_Start event.
Another options is adding a global invalidated_users list on the application, and then checking that user against the invalid list. This list should only contain users that are invalidated after the application has restarted.
Link for Reading All Users Session:
http://weblogs.asp.net/imranbaloch/archive/2010/04/05/reading-all-users-session.aspx

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