ASP.NET Core custom login setup - c#

I have a situation where we use the ASP.NET Core Identity Framework for the Intranet system with hooks into an old CRM database (this database can't be changed without monumental efforts!).
However, we're having customers login to a separate DBContext using identity framework, with an ID to reference back to the CRM. This is in a separate web app with shared projects between them.
This is cumbersome and causes issues when customers are merged in the CRM, or additional people are added to an account etc. Plus we do not need to use roles or any advanced features for the customer login.
So I was thinking to store the username and password in the CRM with the following process:
Generate a random random password.
Use the internal database ID as the salt.
Store the Sha256 hash of the "salt + password" in the password field.
When a customer logs in, we:
Check the Sha256 hash against the salt and given password
If successful, store a session cookie with the fact the customer is logged in: _session.SetString("LoggedIn", "true");
Each request to My Account will use a ServiceFilter to check for the session cookie. If not found, redirect to the login screen.
Questions:
Is this secure enough?
Should we generate a random salt? If stored in the customer table how would it be different to the internal (20 character) customer ID?
Is there a way for the server session cookie to be spoofed? Should we store a hash in the session which we also check on each action?

Is this secure enough?
Generally roll-your-own security is a bad idea because it won't have faced as much scrutiny as an industry standard like Identity Framework. If your application is not life-or-death then maybe this is enough.
Should we generate a random salt?
Yes, salts should always be random. One reason is that when a user changes their password, back to a previous password, if the salt is constant too, then you would get the same hash again, which could be detected.
Another reason is that we don't want the salts to be predictable or sequential. That would make it easier for hackers to generate rainbow tables.
If stored in the customer table how would it be different to the
internal (20 character) customer ID?
I suppose if your customer ID is already a long random guid then that might not matter exactly, but best to play it safe, with cryptographically random disposable salts.
Look at solutions which use RNGCryptoServiceProvider to generate the salt.
Is there a way for the server session cookie to be spoofed?
I don't think a hacker could create a new session just by spoofing. They would need the username & password.
But they could highjack an existing session using Cross-Site Request Forgery.
Should we store a hash in the session which we also check on each
action?
I don't think that would help. Your _session.SetString("LoggedIn", "true") value is already stored on the server and is completely inaccessible from the client. The client only has access to the session cookie, which is just a random id. If that LoggedIn session value is true, then a hash wouldn't make it extra true.

Last year I made a custom IUserPasswordStore for a customer. This solution involved Microsoft.AspNetCore.Identity.UserManager which handles password hashing behind the scenes, no custom password handling required. You will be responsible for storing hashed password in db along with other user properties.
I cannot publish the code in its entirety, it is not my property, but I can sketch up the main parts.
First, we need the IdentityUser:
public class AppIdentityUser : IdentityUser<int>
{
public string Name { get; set; }
}
Then an implementation if IUserPasswordStore
public class UserPasswordStore : IUserPasswordStore<AppIdentityUser>
{
private readonly IUserRepo _userRepo; // your custom user repository
private readonly IdentityErrorDescriber _identityErrorDescriber;
public UserPasswordStore(IUserRepo userRepo, IdentityErrorDescriber identityErrorDescriber)
{
_userRepo = userRepo;
_identityErrorDescriber = identityErrorDescriber;
}
public Task<IdentityResult> CreateAsync(AppIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// if email exists, fail
if (_userRepo.GetByEmailAddress(user.Email) != null)
{
return Task.FromResult(IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(user.Email)));
}
// ... convert AppIdentityUser to model class
//
_userRepo.Save(userModel);
return Task.FromResult(IdentityResult.Success);
}
... implementation of the rest of IUserPasswordStore<AppIdentityUser> comes here
}
Inject this into code for identity user CRUD-operations, e.g. user management controller:
UserManager<AppIdentityUser>
Sample code for changing password (sorry for the nesting)
var result = await _userManager.RemovePasswordAsync(identityUser);
if (result.Succeeded)
{
result = await _userManager.AddPasswordAsync(identityUser, model.Password);
if (result.Succeeded)
{
var updateResult = await _userManager.UpdateAsync(identityUser);
if (updateResult.Succeeded)
{
... do something
}
}
}
Inject this into LoginController:
SignInManager<AppIdentityUser>
We also need an implementation of
IRoleStore<IdentityRole>.
If authorization is not required, leave all methods empty.
In Startup#ConfigureServices:
services.AddIdentity<AppIdentityUser, IdentityRole>().AddDefaultTokenProviders();
services.AddTransient<IUserStore<AppIdentityUser>, UserPasswordStore>();
services.AddTransient<IRoleStore<IdentityRole>, RoleStore>();
services.Configure<CookiePolicyOptions>(options => ...
services.Configure<IdentityOptions>(options => ...
services.ConfigureApplicationCookie(options => ...
In Startup#Configure:
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
See also https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-custom-storage-providers?view=aspnetcore-3.1

Related

Additionally password protect an ASP.NET Core MVC website hosted as Azure WebApp with existing authentication

I have an existing ASP.NET Core MVC application with ASP.NET Core Identity where I use a combination of signInManager.PasswordSignInAsync and [Authorize] attributes to enforce that a user is logged in to website, has a certain role et cetera. This works fine locally and in an Azure WebApp.
Now, I want to publish a preview version of my application to another Azure WebApp. This time, I want each visitor to enter another set of credentials before anything from the website is being shown. I guess I'd like to have something like an .htaccess / BasicAuthenication equivalent. However, after a user entered the first set of credentials, he should not be logged in since he should need to use the normal login prodecure (just as in the live version which is publicly accessible but this has certain pages which require the user to be logged in). Basically, I just want to add another layer of password protection on top without impacting the currently existing authentication.
Given that I want allow access to anyone with the preview password, the following solutions do not seem to work in my case:
Limit the access to the WebApp as a firewall setting. The client IPs will not be from a certain IP range and they will be dynamically assigned by their ISP.
Use an individual user account with Azure AD in front. This might be my fallback (although I'm not sure on how to implement exactly) but I'd rather not have another set of individual user credentials to take care. The credentials could even be something as simple as preview // preview.
Is there a simple way like adding two lines of codes in the Startup class to achieve my desired second level of password protection?
You can do a second auth via a basic auth, something simple and not too much code. You will need a middleware that will intercept/called after the original authentication is done
Middleware
public class SecondaryBasicAuthenticationMiddleware : IMiddleware
{
//CHANGE THIS TO SOMETHING STRONGER SO BRUTE FORCE ATTEMPTS CAN BE AVOIDED
private const string UserName = "TestUser1";
private const string Password = "TestPassword1";
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
//Only do the secondary auth if the user is already authenticated
if (!context.User.Identity.IsAuthenticated)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic "))
{
// Get the encoded username and password
var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();
// Decode from Base64 to string
var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));
// Split username and password
var username = decodedUsernamePassword.Split(':', 2)[0];
var password = decodedUsernamePassword.Split(':', 2)[1];
// Check if login is correct
if (IsAuthorized(username, password))
{
await next.Invoke(context);
return;
}
}
// Return authentication type (causes browser to show login dialog)
context.Response.Headers["WWW-Authenticate"] = "Basic";
// Return unauthorized
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
await next.Invoke(context);
}
}
//If you have a db another source you want to check then you can do that here
private bool IsAuthorized(string username, string password) =>
UserName == username && Password == password;
}
In startup -> Configure (make sure you add this after your existing authentication and authorization)
//Enable Swagger and SwaggerUI
app.UseMiddleware<SecondaryBasicAuthenticationMiddleware>(); //can turn this into an extension if you wish
app.UseAuthentication();
app.UseAuthorization();
In Startup -> ConfigureServices register the middleware
services.AddTransient<SecondaryBasicAuthenticationMiddleware>();
And chrome should pop up a basic auth dialog like this

How to invalidate tokens after password change

I am working on an API that uses JWT token auth. I've created some logic behind it to change user password with a verification code & such.
Everything works, passwords get changed. But here's the catch:
Even if the user password has changed and i get a new JWT token when authenticating...the old token still works.
Any tip on how i could refresh/invalidate tokens after a password change?
EDIT: I've got an idea on how to do it since i've heard you can't actually invalidate JWT tokens.
My idea would be to create a new user column which has something like "accessCode" and store that access code in the token. Whenever i change the password i also change accessCode (something like 6 digit random number) and i implement a check for that accessCode when doing API calls (if the accesscode used in the token doesnt match the one in the db -> return unauthorized).
Do you guys think that would be a good approach or is there some other way ?
The easiest way to revoke/invalidate is probably just to remove the token on the client and pray nobody will hijack it and abuse it.
Your approach with "accessCode" column would work but I would be worried about the performance.
The other and probably the better way would be to black-list tokens in some database. I think Redis would be the best for this as it supports timeouts via EXPIRE so you can just set it to the same value as you have in your JWT token. And when the token expires it will automatically remove.
You will need fast response time for this as you will have to check if the token is still valid (not in the black-list or different accessCode) on each request that requires authorization and that means calling your database with invalidated tokens on each request.
Refresh tokens are not the solution
Some people recommend using long-lived refresh tokens and short-lived access tokens. You can set access token to let's say expire in 10 minutes and when the password change, the token will still be valid for 10 minutes but then it will expire and you will have to use the refresh token to acquire the new access token. Personally, I'm a bit skeptical about this because refresh token can be hijacked as well: http://appetere.com/post/how-to-renew-access-tokens and then you will need a way to invalidate them as well so, in the end, you can't avoid storing them somewhere.
ASP.NET Core implementation using StackExchange.Redis
You're using ASP.NET Core so you will need to find a way how to add custom JWT validation logic to check if the token was invalidated or not. This can be done by extending default JwtSecurityTokenHandler and you should be able to call Redis from there.
In ConfigureServices add:
services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect("yourConnectionString"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.SecurityTokenValidators.Clear();
// or just pass connection multiplexer directly, it's a singleton anyway...
opt.SecurityTokenValidators.Add(new RevokableJwtSecurityTokenHandler(services.BuildServiceProvider()));
});
Create your own exception:
public class SecurityTokenRevokedException : SecurityTokenException
{
public SecurityTokenRevokedException()
{
}
public SecurityTokenRevokedException(string message) : base(message)
{
}
public SecurityTokenRevokedException(string message, Exception innerException) : base(message, innerException)
{
}
}
Extend the default handler:
public class RevokableJwtSecurityTokenHandler : JwtSecurityTokenHandler
{
private readonly IConnectionMultiplexer _redis;
public RevokableJwtSecurityTokenHandler(IServiceProvider serviceProvider)
{
_redis = serviceProvider.GetRequiredService<IConnectionMultiplexer>();
}
public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters,
out SecurityToken validatedToken)
{
// make sure everything is valid first to avoid unnecessary calls to DB
// if it's not valid base.ValidateToken will throw an exception, we don't need to handle it because it's handled here: https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L107-L128
// we have to throw our own exception if the token is revoked, it will cause validation to fail
var claimsPrincipal = base.ValidateToken(token, validationParameters, out validatedToken);
var claim = claimsPrincipal.FindFirst(JwtRegisteredClaimNames.Jti);
if (claim != null && claim.ValueType == ClaimValueTypes.String)
{
var db = _redis.GetDatabase();
if (db.KeyExists(claim.Value)) // it's blacklisted! throw the exception
{
// there's a bunch of built-in token validation codes: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/7692d12e49a947f68a44cd3abc040d0c241376e6/src/Microsoft.IdentityModel.Tokens/LogMessages.cs
// but none of them is suitable for this
throw LogHelper.LogExceptionMessage(new SecurityTokenRevokedException(LogHelper.FormatInvariant("The token has been revoked, securitytoken: '{0}'.", validatedToken)));
}
}
return claimsPrincipal;
}
}
Then on your password change or whatever set the key with jti of the token to invalidate it.
Limitation!: all methods in JwtSecurityTokenHandler are synchronous, this is bad if you want to have some IO-bound calls and ideally, you would use await db.KeyExistsAsync(claim.Value) there. The issue for this is tracked here: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/468 unfortunately no updates for this since 2016 :(
It's funny because the function where token is validated is async: https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L107-L128
A temporary workaround would be to extend JwtBearerHandler and replace the implementation of HandleAuthenticateAsync with override without calling the base so it would call your async version of validate. And then use this logic to add it.
The most recommended and actively maintained Redis clients for C#:
StackExchange.Redis (also used on stackoverflow) (Using StackExchange.Redis in a ASP.NET Core Controller)
ServiceStack.Redis (commercial with limits)
Might help you to choose one: Difference between StackExchange.Redis and ServiceStack.Redis
StackExchange.Redis has no limitations and is under the MIT license.
So I would go with the StackExchange's one
The simplest way would be: Signing the JWT with the users current password hash which guarantees single-usage of every issued token. This is because the password hash always changes after successful password-reset.
There is no way the same token can pass verification twice. The signature check would always fail. The JWT's we issue become single-use tokens.
Source- https://www.jbspeakr.cc/howto-single-use-jwt/
The following approach brings together the best of each approach proposed previously:
Create the column "password_id" in the "user" table.
Assign a new UUID to "password_id" when creating a user.
Assign a new UUID to "password_id" every time the user changes his password.
Sign the authorization JWTs using the "password_id" of the respective user.
If more performance is needed, simply store the "password_id" of the users in Redis.
Advantages of this approach:
If a user changes his password all JWTs existing up to that moment will automatically become invalid forever.
It does not matter if a user changes his password to an old one.
It is not necessary to store the JWTs in the server side.
It is not necessary to add any extra data in the JWT payload.
The implementation using Redis is very simple.

Add 2fa authenticator to user

I have been trying to work out how to enable 2f login with Google Authentication in my Identity server 4 application.
2fa works fine with both email and phone.
if i check
var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user);
it has two email and phone. I am assuming that this would be the two factor providers that have been set up for this user.
Now if i check _usermanager again there is a field called tokenproviders. Which appears to contain default, email, phone, and authenticator. I assume these are the ones that Asp .net identity is configured to deal with.
I have worked out how to create the secret needed to genreate the QR code for the authecator app. As well has how to build the QR code and to test the code
var code = _userManager.GenerateNewAuthenticatorKey();
var qr = AuthencatorHelper.GetQrCodeGoogleUrl("bob", code, "My Company");
var user = await _signInManager.TwoFactorAuthenticatorSignInAsync(codeFromAppToTestWith, true, false);
if (user == null)
{
return View("Error");
}
Now the problem. I have gone though every method I can find on the user trying to work out how to add another token provider to the user.
How do I assign a new token provider to the user and supply the secret code needed to create the authentication codes?? I am not even seeing any tables in the database setup to handle this information. email and phone number are there and there is a column for 2faenabled. But nothing about authenticator.
I am currently looking into creating a custom usermanager and adding a field onto the application user. I was really hoping someone had a better idea.
From what I can see, you are generating a new authenticator key each time the user needs to configure an authenticator app:
var code = _userManager.GenerateNewAuthenticatorKey();
You should be aware that using GenerateNewAuthenticatorCodeAsync will not persist the key, and thus will not be useful for 2FA.
Instead, you need to generate and persist the key in the underlying storage, if it not already created:
var key = await _userManager.GetAuthenticatorKeyAsync(user); // get the key
if (string.IsNullOrEmpty(key))
{
// if no key exists, generate one and persist it
await _userManager.ResetAuthenticatorKeyAsync(user);
// get the key we just created
key = await _userManager.GetAuthenticatorKeyAsync(user);
}
Which will generate the key if not already done and persist it in the database (or any storage configured for Identity).
Without persisting the key inside the storage, the AuthenticatorTokenProvider will never be able to generate tokens, and will not be available when calling GetValidTwoFactorProvidersAsync.

Clarifications and peer review regarding authentication and roles of my web application

I am trying to learn basic security and access limitations on ASP MVC.
So far, i have read/watched tutorials but all of them seems different from one another. If i will search something, it will lead me to another implementation which is totally different from what i have.
I implemented Authentication and custom role provider and i have some questions regarding how things work. Majority of explanations that i found from the internet seems overly complicated or outdated.
This is how i implemented my authentication.
login controller:
[HttpGet]
[ActionName("login")]
public ActionResult login_load()
{
return View();
}
[HttpPost]
[ActionName("login")]
public ActionResult login_post(string uname,string pword)
{
using (EmployeeContext emp = new EmployeeContext())
{
int success = emp.login.Where(x => x.username == uname && x.password == pword).Count();
if (success == 1)
{
FormsAuthentication.SetAuthCookie(uname, false);
return RedirectToAction("Details", "Enrollment");
}
return View();
}
}
Then i protected most of my controllers with [Authorize]
Question #1
What's the purpose of FormsAuthentication.SetAuthCookie(uname, false); and what should i typicalfly use it for? would it be alright to store the username. Do i need it for comparison later on?(further security?). It says here that Authentication ticket will be given to the username. Are those the ones with random letters?
--
After that, i decided to dive deeper and implemented a custom role provider
from roleprovider.cs(I only implemented 2 methods so far)
public override string[] GetRolesForUser(string username)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
return null;
}
var cacheKey = username;
if (HttpRuntime.Cache[cacheKey] != null)
{
return (string[])HttpRuntime.Cache[cacheKey];
}
string[] roles = new string[] { };
using (MvcApplication6.Models.EmployeeContext emp = new MvcApplication6.Models.EmployeeContext())
{
roles = (from a in emp.login
join b in emp.roles on a.role equals b.id
where a.username.Equals(username)
select b.role).ToArray<string>();
if (roles.Count() > 0)
{
HttpRuntime.Cache.Insert(cacheKey, roles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinute), Cache.NoSlidingExpiration);
}
}
return roles;
}
Question #2
I am kinda confused here and i need a deep clarification: so what is basically the purpose of the cacheKey and from my example, i just made it equal to uname since i have no idea what's going on.
Question #3
Why is it returned (string[])HttpRuntime.Cache[cacheKey]; if the value is null? when is it returned and who is receiving it?
Question #4
After getting the value the list of roles from the database, this function will be called HttpRuntime.Cache.Insert(cacheKey, roles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinute), Cache.NoSlidingExpiration);. So from what i see, the roles are being inserted into the cache? is it for checking the login type later on?
Question #5
from this lines of code:
public override bool IsUserInRole(string uname, string roleName)
{
var userRoles = GetRolesForUser(uname);
return userRoles.Contains(roleName);
}
When are they exactly triggered and who provides the parameters? is the roleName from the cache?
I am having a hard time visualizing what's happening under the hood. Explanations/Referrals will be very helpful.
What's the purpose of FormsAuthentication.SetAuthCookie()?
This is ASP.NET FormsAuthentication's built-in method for dealing with authentication cookies.
How does cookie based authentication work?
Explained: Forms Authentication in ASP.NET 2.0
Basically, it's doing the hard work for you; creating a cookie for a specific user, giving it to them and then using it to recognise the same user in the future. You want to use this function to log a user in (if they enter correct credentials).
The string parameter is for a username. Yes, you can use username.
The bool parameter is for if you want the cookie to be persistent. That is, keep them logged in even if they close the browser (whether or not to use a session).
By using FormsAuthentication in this way, ASP.NET will automatically detect the user again when they visit subsequent pages.
What is basically the purpose of the cacheKey?
The Cache component of the HttpRuntime is for managing a "box" of objects that you might retrieve frequently but don't want to be hitting the database all the time for.
The Cache is implemented as a kind of Key-Value Pair. The cacheKey in your example is a key in the Key-Value collection. You can think of it like other similar data structures used in other languages.
{
"carlobrew": {
"roles": {
"Name": "Administrator"
}
}
}
So you're basically "saving" the roles of the user carlobrew in a container so that you can get them again later. The key in a Key-Value Pair is used to refer back to the data that you put in there. The key you are using to refer back to the saved information is the uname; that is, the username.
The key in Key-Value Pairs is unique, so you cannot have two keys called carlobrew.
Why is it returned (string[])HttpRuntime.Cache[cacheKey]; if the value is null?
There are two steps to using a typical "cache box" like this.
If we find the key (such as the user carlobrew) then we can simply return the data straight away. It's not if the value is null. It's if the value is not null. That's why the code is if (HttpRuntime.Cache[cacheKey] != null).
If the key cannot be found (that is, we don't have the key for carlobrew), well then we have to add it ourselves, and then return it.
Since it's a cache, ASP.NET MVC will automatically delete things from the cache when the timer expires. That's why you need to check to see if the data is null, and re-create it if it is.
The "who is receiving it" is whichever object is responsible for calling the GetRolesForUser() method in the first place.
So from what i see, the roles are being inserted into the cache?
Yes.
Basically, if the data isn't in the cache, we need to grab it from the database and put it in there ourselves, so we can easily get it back if we call the same method soon.
Let's break it down. We have:
Insert(cacheKey, roles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinute), Cache.NoSlidingExpiration);
Insert is the method. We're calling this.
cacheKey is the key part of the Key-Value Pair. The username.
roles is the object that we want to store in cache. The object can be anything we want.
DateTime.Now.AddMinutes(_cacheTimeoutInMinute) is telling ASP.NET MVC when we want this data to expire. It can be any amount of time that we want. I'm not sure what the variable _cacheTimeoutInMinute maybe it's 5 or 15 minutes.
Cache.NoSlidingExpiration is a special flag. We're telling ASP.NET that, when we access this data, don't reset the expiration timer back to its full. For example, if our timer was 15 mins, and the timer was about to expire with 1 minute to go, if we were using a sliding expiration and tried to access the data, the timer would reset back to 15 minutes and not expire the data.
Not sure what you mean by "is it for checking the login type later on". But no, there isn't any checking of login type here.
IsUserInRole
You would probably call this when the user is trying to do something. For example, if the user goes to /Admin/Index page, then you could check to see if the user is in the Administrator role. If they aren't, you'd return a 401 Unauthorized response and tell you the user they aren't allowed to access that page.
public Controller Admin
{
public ActionResult Index()
{
if (!IsUserInRole("Administrator"))
{
// redirect "not allowed"
}
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.

Categories