WebSecurity.CurrentUserName and User.Identity.Name are null after logging in - asp.net-mvc-4

I'm stating to figure out SimpleMembership for my ASP.Net MVC 4 site. I've augmented UserProfile with a custom property, AccountTypeId. I've updated the database table with the augmented property and can save data to the database when registering. I'm a bit confused about how to retrieve data about the user once they have logged in.
In my Account controller, I have a Login action that gets posted to when a user logs in. Here's my code:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
{
var userName = WebSecurity.CurrentUserName;
var identityName = User.Identity.Name;
var currentuserid = WebSecurity.GetUserId(model.UserName);
var context = new UsersContext();
var user = context.UserProfiles.SingleOrDefault(u => u.UserId == currentuserid);
var accountTypeId = user.AccountTypeId;
return RedirectToLocal(returnUrl);
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
WebSecurity.CurrentUserName and User.Identity.Name are both empty strings, however, I can retrieve the UserId using WebSecurity.GetUserId(model.UserName) and can therefore retrieve the user data and I can get accountTypeId.
What's strange is User.Identity.Name gets displayed on my page when it's being called from a .cshtml page after the user is redirected to the landing page. So, somewhere in between the Login action of my controller and the destination page, User.Identity is getting set with data.
I'm assuming since I'm past the WebSecurity.Login check, that WebSecurity would have information about the logged in user, but it doesn't seem to be that way.
What am I missing?

The username is written to a cookie. In order for cookie data to be available, it must be placed into the Response and sent back to the browser. Upon the next request, the cookie value will be read and used to populate the User.Identity.Name property.
In other words, the User.Identity.Name property should be an empty string until after your Redirect call. This is the purpose of redirecting after signing on: to write the cookie to the browser so that subsequent requests will treat the user as signed on.

Related

First login to web site after Azure AD authentication

I have an Asp.Net core 3.1 MVC web application. I use EF and Identity
I'm in the process of migrating the authentication process to Azure AD (organizational directory)
I need to find a way to know when the user is login to my site for the first time.
I want to add user-information such as prefered-color, name etc. and to store it in my local DB
My goal is to know when the user logs-in for the first time and redirect him to the profile page for the extra details
Basically, I do it with OnTicketReceived event.
In this event I get the current user after the login process completed, I get the user details from the token and check if the user already in my local DB.
Now I need to redirect it to his profile page if the user record was not found.
I cannot change the redirect URL in OnTicketReceived event, I decided to create a middleware as follows:
In OnTicketReceived I check if the user in my local DB and add "NewUser" key to the session
Added NewUserMiddleware that runs and checks the session key. If "NewUser" key exits, I generate a new record in my local DB and redirect to the profile page, then I remove the key
It's working but I'm sure that there is an easier way to do it...
I don't want to create a register button - the site is internal to the organization and I don't need to create an anonymous register page
Some code stuff:
in OnTicketReceived:
var db = context.HttpContext.RequestServices.GetService<MyDbContext>();
var user = db.Users.SingleOrDefault(u => u.Email == email);
if (user == null)
{
context.HttpContext.Session.SetInt32("NewUser", 1);
}
NewUserMiddleware:
public async Task InvokeAsync(HttpContext context)
{
int? isNewUser = context.Session.GetInt32("NewUser");
if(isNewUser.HasValue && isNewUser.Value == 1)
{
MyDbContext _dbContext = context.RequestServices.GetService(typeof(MyDbContext)) as MyDbContext;
var identity = context.User.Identity as ClaimsIdentity;
string email = identity.FindFirst(c => c.Type == "preferred_username")?.Value;
string name = identity.FindFirst(c => c.Type == "name")?.Value;
User user = new User()
{
Email = email,
UserName = name,
Color = GenerateColor()
};
_dbContext.Users.Add(user);
_dbContext.SaveChanges();
context.Session.Remove("NewUser");
await context.Session.CommitAsync();
context.Response.Redirect("account/profile", true);
}
await _next(context);
}

How to extend and validate session in ASP.NET Core Identity?

We want to offer the users to manage their login sessions.
This worked so far pretty easy with ASP.NET Core and WITHOUT the Identity Extensions.
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#react-to-back-end-changes
But how can we invoke this validation with ASP.NET Core Identity?
Problem we have:
How do we store login-session-based information like Browser Version, Device Type and User Position? Do we extend any type or what is the idea?
How do we dynamically set the cookie expiration based on a specific user?
How do we invalidate the Cookie from the backend (like the link above shows)?
How do we required additional password-prompts for special functions?
It feels the ASP.NET Core Identity is still not that extensible and flexible :(
Unfortunately, this area of ASP.NET Identity is not very well documented, which I personally see as a risk for such a sensitive area.
After I've been more involved with the source code, the solution seems to be to use the SignIn process of the SignIn Manager.
The basic problem is that it's not that easy to get your custom claims into the ClaimsIdentity of the cookie. There is no method for that.
The values for this must under no circumstances be stored in the claims of the user in the database, as otherwise every login receives these claims - would be bad.
So I created my own method, which first searches for the user in the database and then uses the existing methods of the SignInManager.
After having a ClaimsIdentity created by the SignIn Manager, you can enrich the Identity with your own claims.
For this I save the login session with a Guid in the database and carry the id as a claim in the cookie.
public async Task<SignInResult> SignInUserAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
{
DateTimeOffset createdLoginOn = DateTimeOffset.UtcNow;
DateTimeOffset validTo = createdLoginOn.AddSeconds(_userAuthOptions.ExpireTimeSeconds);
// search for user
var user = await _userManager.FindByNameAsync(userName);
if (user is null) { return SignInResult.Failed; }
// CheckPasswordSignInAsync checks if user is allowed to sign in and if user is locked
// also it checks and counts the failed login attempts
var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
if (attempt.Succeeded)
{
// TODO: Check 2FA here
// create a unique login entry in the backend
string browserAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
Guid loginId = await _eventDispatcher.Send(new AddUserLoginCommand(user.Id, user.UserName, createdLoginOn, validTo, browserAgent));
// Write the login id in the login claim, so we identify the login context
Claim[] customClaims = { new Claim(CustomUserClaims.UserLoginSessionId, loginId.ToString()) };
// Signin User
await SignInWithClaimsAsync(user, isPersistent, customClaims);
return SignInResult.Success;
}
return attempt;
}
With each request I can validate the ClaimsIdentity and search for the login id.
public class CookieSessionValidationHandler : CookieAuthenticationEvents
{
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
ClaimsPrincipal userPrincipal = context.Principal;
if (!userPrincipal.TryGetUserSessionInfo(out int userId, out Guid sessionId))
{
// session format seems to be invalid
context.RejectPrincipal();
}
else
{
IEventDispatcher eventDispatcher = context.HttpContext.RequestServices.GetRequiredService<IEventDispatcher>();
bool succeeded = await eventDispatcher.Send(new UserLoginUpdateLoginSessionCommand(userId, sessionId));
if (!succeeded)
{
// session expired or was killed
context.RejectPrincipal();
}
}
}
}
See also
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#react-to-back-end-changes

OWIN Authentication Cookie information and forcing login even when not expired

I want to provide two pieces of functionality to an MVC 5 (OWIN) website for the Authentication Cookies:-
Show cookie expiration for each user
Force a login, even if a cookie is not expired
Once a user is logged in, the cookie and its contained claims are on their browser.
I've managed to get the cookie information, although it does this on every visit to the server.
As far as forcing a login is concerned, I either have to update the cookie (not possible if the user isn't interacting with the site) or look for a user by name on each login.
My answer is below - if anyone has a better one, please let me know
In the OWIN start-up class I added a custom identity validation method. An administrator can set a field in the Identity Database to force a login next time around. A downside is a hit on the database each time a user visits a web page.
If a user must login this time, Forced Login flag is cleared, the user logged off and redirected to the Home page which then goes to the login page (and redirect back to the Home page after login).
This code also retrieves the cookie information for display in the list of users that an administrator can view.
private static Task MyCustomValidateIdentity(CookieValidateIdentityContext context)
{
var db = ApplicationDbContext.Create();
var userName = context.Identity.GetUserName();
var user = (from u in db.Users where u.UserName == userName select u).FirstOrDefault();
if (user != null)
{
if (user.ForceLoginNextTime)
{
user.ForceLoginNextTime = false;
db.SaveChanges();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
context.Response.Redirect("/Home");
}
}
var i = (from u in __userInfo where u.userName == userName select u).FirstOrDefault();
if (i == null)
{
i = new UserInfo();
__userInfo.Add(i);
}
i.userName = userName;
i.CookieIssuedUtc = context.Properties.IssuedUtc;
i.CookieExpiresUtc = context.Properties.ExpiresUtc;
i.CookieIsPersistent = context.Properties.IsPersistent;
return Task.FromResult(0);
}

userManager.FindByName does not return roles

I am using OpenIddict for token authentication. Yesterday when I call userManager.FindByNameAsync(request.Username) I get User with Roles.
Today I get user with Roles property count = 0.
I tried to load roles with await userManager.GetRolesAsync(user); and I get array with count 3. That means user has roles.
I do not know what changed, but how can I load user with roles with FindByNameAsync function?
Complete code:
[HttpPost("token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType())
{
var user = await userManager.FindByNameAsync(request.Username); //roles count is 0
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The email/password couple is invalid."
});
}
var roles = await userManager.GetRolesAsync(user); //roles count is 3
but how can I load user with roles with FindByNameAsync function?
You can't (at least not without implementing your own IUserStore).
Under the hood, the default ASP.NET Core Identity store implementation (based on EF) doesn't eagerly load the navigation properties associated with the user entity for performance reasons, which is why the Roles property is not populated.
To load the roles associated with a user, use UserManager.GetRolesAsync(user);.

Redirecting to an Action Method from another Action Method in the same controller

I am a Newbie in asp.net and currently I am doing a web page application in MVC4 with Login functionality.
My Index action method looks like this-
public ActionResult Index()
{
var PageModelList1 = new DataAccessLayer.DataAccess().GetPageInfo();
ViewData["MenuList"] = PageModelList1.PageModelList;
return View();
}
and my LogIn action method looks like-
[HttpPost]
public ActionResult LogIn(LogInModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var PageModelList1 = new DataAccessLayer.DataAccess().GetPageInfo(model.UserName,model.Password);
ViewData["MenuList"] = PageModelList1.PageModelList;
return RedirectToAction("Index", "MyController");
}
ModelState.AddModelError("", "login failed");
return PartialView("_LogIn", model);
}
what I need is, when I Login successfully, the RedirectToAction("Index", "Deimos") should take place but the 'MenuList' there should be the new 'MenuList' from LogIn action method. How could I do it?
RedirectToAction will send a 302 response to the browser with the new url as the location header value and browser will make a totally new request to go to that page. This new request has no idea what you did in the previous request. So ViewData will not work. You may consider using TempData.
But TempData's life is only until the next request. After that it is gone. So if you want something on all the subsequent requests(like a menu to be shown to user), I suggest you read it from a database table every time you load the page. You can store the items to a cache after the first read to avoid constant hit(s) to the database if you are worried about that.
Another option is to set the menu items to Session variables and read from there. I am not a big fan of setting stuff like that to session. I prefer to read it from a cache (in which data was loaded from a db call) or so.