ASP.NET MVC authenticates and authorizes non-existent users - asp.net

I'm using the DropCreateDatabaseAlways Initializer so my database gets destroyed every time I start the application (at least I hope so). The funny thing is I still see myself as logged in. I get pass the Authorize attribute and can do dangerous stuff. This is probably because of leftover cookies from previous testing.
Registration/login part of my app is the MVC 4 Internet Application template untouched. Shouldn't ASP.NET check the cookie values against users saved in the DB? WebSecurity.IsAuthenticated returns true and WebSecurity.CurrentUserName returns the name.
The only thing working as expected is WebSecurity.CurrentUserId which returns -1. I'm a newbie so I can only guess this is because UserId isn't stored in the cookie and must be retrieved from the database.
Am I right? If so, does it mean I should always use WebSecurity.CurrentUserId to determine whether a user is logged in? WebSecurity.IsAuthenticated and User.Identity.IsAuthenticated seem pretty useless in that case. I can delete a user's account and he or she remains unaffected. What should be done differently if I am wrong?

If you want to realiably check whether a user has not been deleted, you just have to consult the database.
Note that users and administrators work concurrently. This means that a user can be deleted just a second after he has been authenticated. A cookie can be then even one or two seconds old (!) and the user could probably have been just deleted.
In a most pessimistic scenario, a user types a valid username and password and is succesfully logged in and get the "sorry, your account has been deleted" just one request later (because the account has really just been deleted).

There will be a small window where if a user is deleted and they are still logged in that they can still access the site. Since most actions require a validate user id, you can simply throw an excpetion and log the user out.
Normally the database does not get blown away on each build, so I'm guessing this is not a use case SimpleMembership was coded for. You can of course check for this. I'll make another assumption that you are not closing your browser when you rebuild the site and deploy the new database. In a real world scenario these things just don't happen. The database is never blown away and the user id is never lost.
Generally once you've logged in the user, the user is not authenticated anytime after that (unless they have logged out, or the session has expired). That's the point of login. The authentication cookie is an indication that the authentication happened and was successful. The assumption going forward is the user has access to your site and is not reauthenticated.

As long as the authentication session remains open (i.e. browser now closed), the session cookie remains active and MVC assumes that the user is still valid.
Remove the cookie by using FormsAuthentication.LogOff() if the user is authenticated (User.Identity.IsAuthenticated == true) and there's no valid user in UserTable (WebSecurity.CurrentUserId == -1).

I had the same issue, but solved it by specifying the required role(s) in the Authorize attribute. As soon as you do this, it start getting to the database and fails with the "user does not exist" error, which is what you want.
[Authorize(Roles = "Customer")]
public class DashboardController : Controller

In normal case adding Session.Abandon(); to to your LogOff action in AccountController would do the job of clearing session:
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Home");
}
So I think you can try adding Session.Abandon(); in init code, where you use DropCreateDatabaseAlways to clear session each time.

The ticket issuing time is usually encrypted within the authentication cookie then you can use it to require re-authentication to sensitive areas (inbox/billing etc.) if more than X time passed since login.
If you insist on invalidating all current auth tickets upon application changes (i.e database/configuration) you can change this settings in your web.config:
<machineKey validationKey="..." decryptionKey="" />

Related

Force reauthentication after user permissions have been changed

In my application I can change user permissions and roles in backend.
When a user is logged in and I remove a role of the user, the user can still access content which he actually is not permitted to access anymore, because he is missing the role. The changes take effect only when the user reauthenticates himself with logout/login.
So my question is, can I access the session of a logged in user (not me)? I know I can access my own session and destroy it which forces me to login again. But I want to get the session of any user who is logged in. Is this possible? I could not find any resources about that.
I use PdoSessionStorage with symfony2.1 and fosuserbundle.
Make your user class implement Symfony\Component\Security\Core\User\EquatableInterface.
If you return false from the isEqualTo() method, the user will be reauthenticated. Use that method to compare only those properties that when changed should force reauthentication — roles in your case.
You can get around this issue by following an approach similar to what I did:
When user logs in, store all permissions in session along with a checksum of those permissions.
Store the same checksum in a database, or on disk, against that user ID
Whenever the user makes a request, verify that the checksum on disk matches the one in session for that user. If it is different, reload the permissions into the user's session
When you change the permissions, update the checksum in the database (or on disk) that is stored against that user. This will trigger a resync on their next request.

Session Expires and User is no longer valid

I cache information about the currently logged in user in the session. This info lazy loads whenever a CurrentUser property on my global application class is used. It does this by calling GetUser() on my custom implementation of MembershipProvider, which either loads the user up from the session, or loads the user from the DB and throws the user object in the session.
How should I handle this scenario?
User logs in.
Administrator deletes user (or deactivates...the point is
they can't log in any more).
User's session expires.
User navigates to a page or makes a request, or whatever.
Currently if this scenario occurs, NullReferenceExceptions are thrown all over the place, because the ASP .NET framework calls GetUser() which returns nothing because it can't find the user in the database (and there's nothing in the session because it expired).
If your app thinks a user is signed in but the user cannot be found, one option might be to use FormsAuthentication.SignOut() to make ASP.NET forget about the user. They should then be kicked back to the login screen or anonymous mode.
Throw an exception from GetUser() if you're going to return null. Then you can have the Application_Error event trap that specific exception and redirect to your login page.

ASP.Net Membership Services

In our application, we have a need for a user to "impersonate" a different user. Think of it as a hierarchy -- Bob is above Frank in a hierarchy. Bob is logged in, and he needs to do things in the system for a short time as Frank. So, we have given Bob a list of users that report to him, and an impersonate link. He clicks on this link, and, behind the scenes, I log Bob out, and log in as Frank. I also set a session variable that tells me that really Bob is they guy who is the user. Also, Bob (acting as Frank now) has a nice little link at the top of every page that says "Stop Impersonation."
In addition, when Bob is impersonating Frank, Bob is restricted from doing some things, like changing Frank's password.
This was working great, until we encountered a situation where, if the session (I think -- getting confused here) gets destroyed (such as when I copy up new code and dlls to the live site), then when Bob clicks on "Stop Impersonation" he gets redirected to the default page, and is still logged in as Frank, but without the Impersonation session variable. So, now Bob really is logged in as Frank, and can change Frank's password (among other things).
How is it that a session variable (Impersonation) gets destroyed, but I guess the session is still hanging around, because it doesn't make the user log in again?
This is a somewhat serious bug for how our system works (bug in our code, I'm sure, not in .Net). Does anyone have any suggestions for a solution for this?
We are using ASP.Net c#, aspnet membership services, .net 3.5, forms auth...not sure what else you need to know.
EDIT: Updated information. Looks like when "something" happens, for instance, when I recompile some dlls and copy them to the webserver, the session gets dumped. Or, rather, the variables in the session get dumped. The session id stays the same. I do get to check for Session.IsNewSession and it returns true, even though the id is the same as it was before.
Just like Utaal mentioned, Membership Services is separate from Session, so it's forms auth token is still hanging around in the browser, but my session variable telling me that that isn't really the user who is controlling the browser isn't there anymore.
EDIT: Sky, here is what I'm doing to authenticate a user. I can't figure out where I would insert a ticket into this flow:
if (Membership.ValidateUser(txtUserName.Text, txtPassword.Text))
FormsAuthentication.SetAuthCookie(txtUserName.Text, false);
So, where can I slip in a ticket object and set my own information?
Matt,
Use the UserData slot on the forms ticket to store the impersonation information. That is what it is for.
Then your info will not get lost with the session.
If you would like a simple example of creating your own ticket, amongst other things, check this. You may want to focus on the login page and the tickethelper class.
I think your problem is due to the fact that Forms Authentication and Session are two different things and are not interconnected: both of them (usually) use cookies but Forms Authentication stores the encrypted logged-in user directly in the cookie while Session stores information in-process (even if you can change this behaviour) and uses a cookie with a session identifier to retrieve it.
So, when your session information gets lost (or session expires) it isn't really still hanging around (except for the invalid session cookie on the user's pc). On the other hand the Forms Authentication cookie is still valid (ASP.NET decrypts it and authenticates the user for the request).
A possible solution is to detect the creation of a new session (using HttpSessionState.IsNewSession MSDN) and sign out the user (using FormsAuthentication). You can then redirect user to login page.

ASP.NET Problem Caching Roles In Cookie

I'm using ASP.NET Roles with a special role "Must Change Password". If a user has not changed their password for more than 90 days, they are automatically added to this role. This happens during the user login process. Authorization rules then deny that role access to all of the application except the "change password" page.
Generally this works great, but there is a problem when the role cache cookie is used to cache roles. What happens is during the login process, the password last changed date is checked, and if > 90 days, the user is added to the "Must Change Password" role. In the same page request, I subsequently call Roles.IsUserInRole("Must Change Password") to decide whether to redirect the user to the Change Password page or not. This is where it falls down - it seems that with the role cache cookie enabled, Roles.IsUserInRole("Must Change Password") doesn't realise that I have changed role mappings for this user, and returns false. However, on the next page request, Roles.IsUserInRole("Must Change Password") returns true.
This behaviour is fixed by setting cacheRolesInCookie="false", but that seems a high price to pay. Is there another way to fix this problem?
Another , IMHO more elegant, solution is to cast HttpContext.User to RolePrincipal and call SetDirty method after adding a new role to the user (read more on RolePrincipal.SetDirty).
The next call of IsInRole or GetRolesForUser methods should trigger request to your default RoleProvider.
Since you said that the problem exists in the same request, how about also setting an item in the HttpContext.Current.Items collection to indicate that the user must change their password, and check both the cookie and the HttpContext.Current.Items collection later on in the code?
Actually, I've found the problem - it is not a problem with caching roles in cookies, but rather a problem with Roles.IsUserInRole(). If I use the overload Roles.IsUserInRole(username, role) then it works fine, with or without roles cached in a cookie.

Event to capture when a formsauthenticated user is un-authenticated

I'm basically looking for the event that is the opposite of FormsAuthentication_OnAuthenticate. I want to remove some values from the database when a user logs out. I tried putting it into the Session_End event, but it seems that the user is already gone by the time this executes.
Update:
If I can't determine when a specific user is deauthenticated (ie, due to session timeout) then is there a way to get a list of all currently authenticated users? If I could, then in the Session_End I could just remove the records from the DB that are not associated with a currently authenticated users.
Session_End isn't guarenteed to fire - if you're not using InProc sessions for example, then it won't fire at all. If your application recycles or dies, again, it won't fire.
Your best bet would be to have this code in a shared method that you can call from numerous places:
In your LoginStatus control you can set the LoggingOut event - call your method there to handle people who log out sensibly.
If you're using InProc sessions, in your Session_End event, but make sure you check to see if they are logged out already (as you've seen).
If you're not using InProc sessions, you'll need to get a little more creative. Perhaps look at having an event that fires every now and then (perhaps on Session_Start which does fire regardless) that goes through and clears out those users who's last active time is older than the session timeout (as mentioned by Greg).
Unforunately the Membership class gives you some useful details, but not all of them:
GetNumberOfUsersOnline
This will "Gets the number of users currently accessing an application." - great, but the only methods that will get users either:
GetAllUsers // Gets all the users from the storage provider (can be paged)
FindUsersByName
FindUsersByEmail
Sadly none of these have a property to only return "active users" as per the count.
Looking at the members of MembershipUser there isn't a "IsOnline" property - only LastLogonDate and LastActivtyDate - due to the disconnected nature of the web, this is probably as good as you're going to get.
I would imagine you have them logging out via the click of a button or link or something like that. Why not just put the code in that same event / block. Near where you put the FormsAuthentication.SignOut() call.
There is a Session_End handler in the Global.asax in which you could put could that you want to execute when the session expires.
I am not sure that this is what you want though. Session and authentication are two different things. If your authentication technique is providing a FormsAuthenticationTicket to the user (inside a cookie) and that ticket has an expiration, well the expiration of the authentication is controlled via this ticket. It will not be actively managed on the server. Each request the user makes the ticket is provided and the server then determines if the user is still authenticated.
Bottom line is, you can detect when the user's session expires, but you probably won't be able to determine when their authentication expires, unless both expiration values are identical.
If you're using the SQL provider, the aspnet_Users table has a "LastActivityDate" column. If you compare that to the timeout value of forms authentication, you could come up with a list of users are definitely not logged in. Your count would be low if they log out manually with a "log out" link.

Resources