Invalidate ClaimsPrincipal after it has been modified - asp.net-mvc

I am using ASP.NET MVC, Identity2.
I have added "FirstName" Custom ClaimPrincipal:
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, long> manager)
{
var userIdentity = await manager.CreateIdentityAsync(
this,
DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
userIdentity.AddClaim(new Claim("FirstName", FirstName));
return userIdentity;
}
If I update the value of "FirstName", I need to logout and log back in, for the "FirstName" Claim to be updated. Is it possible to invalidate "FirstName" Claim, so it's value is forced to be refreshed?
I have seen this question, which shows how to update the value of Claims, I was wondering if there is easier way to just invalidate them.

When looking at MS built-in template, I noticed that they alway makes a call to SignInManager.SignInAsync, after changing user credentials (e.g. password, 2 Factor Authentication, etc).
I also noticed that the Claims are updated once the user logs out and logs back in... so after changing "FirstName" which is stored in a Claim, I called SignInManager.SignInAsync to re-signin the User... this way, the Claims are updated:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UpdateFirstName(string firstName)
{
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId<long>());
user.FirstName = firstName;
// update FirstName which is stored in a Claim
var result = await UserManager.UpdateAsync(user);
if (result.Succeeded)
{
// re-signin the user, to refresh the Claims
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// you need to redirect after calling SignInAsync, so claims are refreshed
return RedirectToAction("Index");
}
// add some error message...
return View();
}
Note: As shown in the question, I am storing the Claims in Cookie.

Related

MVC Owin external login disconnect database

I have a basic MVC 5 site setup for user accounts and external logins.
For external logins I am using on-premises ADFS OpenID Connect for employees so it should be similar to Azure AD.
So in this case the MVC Identity 2.0 verbiage of External Login is actually for Internal Users (Employees). Employees will use ADFS and the public will have user accounts in the aspnet database.
The normal workflow in the MVC template, adds external users to the aspnet database. The workflow adds them or checks if the users are in the appropriate tables in this database.
I want to disconnect this database for employees since they are already authenticated by ADFS because I can authorize them using our AD and claims to there is no reason to add them to the database.
Here is the standard code in the AccountController.
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
//var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
case SignInStatus.Failure:
default:
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}
Here the loginInfo shows the user is authenticated.
The result variable is false since the user is not in the aspnet database and so would send the user to the ExternalLoginConfirmation view.
If I modify the switch statement to send the user to the returnUrl instead, the Authenticate attribute on that controller action sees the user as not authenticated and starts the process over again since the user is not authenticated according to the built-in workflow.
How do I intercept this workflow and satisfy the Authorize attribute? I have checked these properties and they return false.
var authenticated = HttpContext.User.Identity.IsAuthenticated;
var authenticated2 = HttpContext.Request.IsAuthenticated;
var authenticated3 = HttpContext.GetOwinContext().Authentication;
I was able to disconnect the aspnet database from the workflow by removing the code which checks the database tables.
The new code signs out of the external cookie, generates a new application cookie and copies the claims from the external cookie to the application cookie.
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
//We signed in with an external provider which creates and external cookie but this is not sufficient for application
//authorization so we need to create an application cookie and copy the existing claims and add new claims as needed
var externalClaims = loginInfo.ExternalIdentity.Claims;//get the current external claims for the user
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);//sign out of external authentication
var claims = externalClaims.ToList();//create a new list of the external claims
//add the identityprovider claim since this is needed for the AntiForgeryToken
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", loginInfo.DefaultUserName));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.Name, ClaimTypes.Role);//create new identity
AuthenticationManager.SignIn(identity);//sign in with the new local identity containing an application cookie
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return RedirectToLocal(returnUrl);
}

Why user is null

I would like to ask, why UserId is null (in code below) after user log in.
OR
I need redirect users depending on the they roles. I need make it As Simple As Possible.
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
string UserId = User.Identity.GetUserId(); <------ HERE
HelpRoles hr = new HelpRoles();
returnUrl = hr.CheckUserRoleAndRedirect(UserId);
return RedirectToLocal(returnUrl);
The User object is set based on the decrypted cookies after the request is authenticated, and these cookies aren't set until the next request so the page needs to be refreshed before User.Identity is not null.
For more info, see this post.
To work around this, within the success case you can get the user from the DB using the email address and use the ID on that instead.
Like John Mc said to work around this you have to the get user in the Success case. So you might want something like this before getting UserId
var user = userManager.Find(model.UserName, model.Password);
or
var user = userManager.FindByEmail(model.Email);
or
var user = userManager.FindByName(model.Username);
EDIT
In this case you wouldn't have to use your method as shown below
string UserId = User.Identity.GetUserId(); <------ HERE
because once you get the user object you can get the id field in it like user.Id

Prevent multiple logins

I am trying to block multiple logins with the same user in my application. My idea is to update the security stamp when user signin and add that as a Claim, then in every single request comparing the stamp from the cookie with the one in the database. This is how I've implemented that:
public virtual async Task<ActionResult> Login([Bind(Include = "Email,Password,RememberMe")] LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
SignInStatus result =
await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
switch (result)
{
case SignInStatus.Success:
var user = UserManager.FindByEmail(model.Email);
var id = user.Id;
UserManager.UpdateSecurityStamp(user.Id);
var securityStamp = UserManager.FindByEmail(model.Email).SecurityStamp;
UserManager.AddClaim(id, new Claim("SecurityStamp", securityStamp));
Then in authentication configuration I've added
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = ctx =>
{
var ret = Task.Run(() =>
{
Claim claim = ctx.Identity.FindFirst("SecurityStamp");
if (claim != null)
{
var userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
var user = userManager.FindById(ctx.Identity.GetUserId());
// invalidate session, if SecurityStamp has changed
if (user != null && user.SecurityStamp != null && user.SecurityStamp != claim.Value)
{
ctx.RejectIdentity();
}
}
});
return ret;
}
}
});
As it shows I have tried to compare the claim from the cookie with the one in the database and reject the identity if they are not the same.
Now, each time the user signs in the security stamp gets updated but the value is different in user's cookie which I can't find out why? I am suspicious maybe it the new updated security stamp doesn't get stored in user's cookie?
The solution is somewhat more simple than you have started implementing. But the idea is the same: every time user logs in, change their security stamp. And this will invalidate all other login sessions. Thus will teach users not to share their password.
I have just created a new MVC5 application from standard VS2013 template and successfully managed to implement what you want to do.
Login method. You need to change the security stamp BEFORE you create auth cookie, as after the cookie is set, you can't easily update the values:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// check if username/password pair match.
var loggedinUser = await UserManager.FindAsync(model.Email, model.Password);
if (loggedinUser != null)
{
// change the security stamp only on correct username/password
await UserManager.UpdateSecurityStampAsync(loggedinUser.Id);
}
// do sign-in
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
This way every login will do an update on the user record with the new security stamp. Updating security stamp is only a matter of await UserManager.UpdateSecurityStampAsync(user.Id); - much simplier than you imagined.
Next step is to check for security stamp on every request. You already found the best hook-in point in Startup.Auth.cs but you again overcomplicated. The framework already does what you need to do, you need to tweak it slightly:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// other stuff
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(0), // <-- Note the timer is set for zero
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
The time interval is set for zero - means the framework on every request will compare user's security stamp with the database. If stamp in the cookie does not match the stamp in the database, user's auth-cookie is thrown out, asking them to logout.
However, note that this will bear an extra request to your database on every HTTP request from a user. On a large user-base this can be expensive and you can somewhat increase the checking interval to a couple minutes - will give you less requests to your DB, but still will carry your message about not sharing the login details.
Full source in github
More information in a blog-post
In the past I've used IAuthorizationFilter and static logged-in user collection to achieve this:
public static class WebAppData
{
public static ConcurrentDictionary<string, AppUser> Users = new ConcurrentDictionary<string, AppUser>();
}
public class AuthorisationAttribute : FilterAttribute, IAuthorizationFilter {
public void OnAuthorization(AuthorizationContext filterContext){
...
Handle claims authentication
...
AppUser id = WebAppData.Users.Where(u=>u.Key ==userName).Select(u=>u.Value).FirstOrDefault();
if (id == null){
id = new AppUser {...} ;
id.SessionId = filterContext.HttpContext.Session.SessionID;
WebAppData.Users.TryAdd(userName, id);
}
else
{
if (id.SessionId != filterContext.HttpContext.Session.SessionID)
{
FormsAuthentication.SignOut();
...
return appropriate error response depending is it ajax request or not
...
}
}
}
}
On logout:
WebAppData.Users.TryRemove(userName, out user)

Linking external logins to existing user

I'm quite new to Identity and trying to learn by watching videos in https://channel9.msdn.com/Series/Customizing-ASPNET-Authentication-with-Identity
In the default ASP.Net MVC template, you can link multiple external logins (google, facebook) to your account (through /Manage) if you are already logged in.
But what if the user first logged in to our website using their google account and log out from it and on another day tried to login using their facebook account. Assuming both of their facebook and google accounts uses the same email address, the user will not be able to login to the website because the default template doesn't allow that as UserManager.CreateAsync is going to fail. I know they can change their email and login, but that will create two different accounts for the same user.
var info = await AuthenticationManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
I altered the code in ExternalLoginConfirmation so that it checks if the user exist and ads the new external provider to AspNetUserLogins. Can anyone please tell me if this is the right way to do this? or if there is a better way of doing it.
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await AuthenticationManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
//new code begins
else if (UserManager.FindByEmail(model.Email) != null)
{
var usr = await UserManager.FindByEmailAsync(model.Email);
result = await UserManager.AddLoginAsync(usr.Id, info.Login);
if (result.Succeeded)
{
await SignInManager.SignInAsync(usr, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
//new code ends
AddErrors(result);
}
I'm also fairly new to Identity and have come across the same problem, though my solution is quite different. I don't use Entity Framework and to achieve this I've had to basically rewrite the entire Identity engine with custom classes. I have my own Sql Server tables which are different to those created by EF. EF stores identity in 5 tables: users, roles, userroles, userclaims and userlogins. I only use the first three. In my environment AddLoginAsync is not required because the table doesn't exist. I store all local/external logins and registered users in the user table. Claims are stored as userroles when required.
The way I got around duplicate UserNames and Emails (logging in using different providers with the same registered email addresses) was to remove the validation check for existing usernames and emails before creating a user, using a custom UserValidator. The table allows for duplicates. When logging in I do a custom check for uniqueness based on username/provider (external) or email/passwordhash (local). It seems to be working.

prevent users without confirmed email from logging in ASP.Net MVC with Identity 2

In microsoft Identity 2 there is ability to users can confirm there email addresses I downloaded Identity 2 sample project from here in this project there isn't any difference between users confirmed their emails and who doesn't I want to people how don't confirmed their emails can't login this is what I tried :
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: true);
switch (result)
{
case SignInStatus.Success:
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user != null)
{
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
//first I tried this.
//return LogOff();
HttpContext.Server.TransferRequest("~/Account/LogOff");
return RedirectToAction("Login");
}
}
return RedirectToLocal(returnUrl);
}
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
I tried to force user to Logoff by calling LogOff() action method but It didn't work and user remain authenticated .then I tried to use Server.TransferRequest() but I don't know why it did the job but it redirects users to login page with returnUrl="Account/Logoff"
so after they confirmed their email and tried to login they get logoff I get really confused!!
this is my LogOff() action method:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut();
return RedirectToAction("About", "Home");
}
I have googled it for days without any luck !!!!
Maybe its a little late but I hope it may help others.
Add this
var userid = UserManager.FindByEmail(model.Email).Id;
if (!UserManager.IsEmailConfirmed(userid))
{
return View("EmailNotConfirmed");
}
before
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
The first block of code just checks if the email in the model exists in the database and gets it's id to check if it is not confirmed and if so returns a view to the user wich says so and if it is confirmed just lets the user sign in.
And delete your changes to the result switch like this
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
Instead of moving to another page, why not finish this one and redirect to the right action / view:
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
return RedirectToAction("ConfirmEmailAddress", new { ReturnUrl = returnUrl });
}
You do need an action (and possibly a view) with the name ConfirmEmailAddress though.
There is a solution, which may not be the best approach, but it works. First let me try to clarify why your approach did not work.
In one of the comments it is mentioned, the AuthenticationManager uses cookies. In order to update a cookie you need to send it to the client, using another page. That is why TransferRequest is not going to work.
How to handle the emailverification? The strategy I used:
1) On SignInStatus.Success this means that the user is logged in.
2) When email is not confirmed: send an email to the used e-mailaddress. This is safe since the user already signed in. We are just blocking further access until the e-mail is verified. For each time a user tries to login without having validated the email, a new email (with the same link) is sent. This could be limited by keeping track of the number of sent emails.
3) We cannot use LogOff: this is HttpPost and uses a ValidateAntiForgeryToken.
4) Redirect to a page (HttpGet, authorization required) that displays the message that an e-mail has been sent. On entering sign out the user.
5) For other validation errors, redirect to another method to sign out (HttpGet, authorization required). No view needed, redirect to the login page.
In code: update the code in AccountController.Login to:
case SignInStatus.Success:
{
var currentUser = UserManager.FindByNameAsync(model.Email);
if (!await UserManager.IsEmailConfirmedAsync(currentUser.Id))
{
// Send email
var code = await UserManager.GenerateEmailConfirmationTokenAsync(currentUser.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = currentUser.Id, code = code}, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(currentUser.Id, "Confirm your account", string.Format("Please confirm your account by clicking this link: link", callbackUrl));
// Show message
return RedirectToAction("DisplayEmail");
}
// Some validation
if (true)
{
return RedirectToAction("SilentLogOff");
}
return RedirectToLocal(returnUrl);
}
Add methods to AccountController:
// GET: /Account/SilentLogOff
[HttpGet]
[Authorize]
public ActionResult SilentLogOff()
{
// Sign out and redirect to Login
AuthenticationManager.SignOut();
return RedirectToAction("Login");
}
// GET: /Account/DisplayEmail
[HttpGet]
[Authorize]
public ActionResult DisplayEmail()
{
// Sign out and show DisplayEmail view
AuthenticationManager.SignOut();
return View();
}
DisplayEmail.cshtml
#{
ViewBag.Title = "Verify e-mail";
}
<h2>#ViewBag.Title.</h2>
<p class="text-info">
Please check your email and confirm your email address.
</p>
You'll notice that the user cannot reach other pages until email is verified. And we are able to use the features of the SignInManager.
There is one possible problem (that I can think of) with this approach, the user is logged in for the time that the email is sent and the user is being redirected to the DisplayMessage view. This may not be a real problem, but it shows that we are not preventing the user from logging in, only denying further access after logging in by automatically logging out the user.
=== Update ====
Please note that exceptions have to be handled properly. The user is granted access and then access is revoked in this scenario. But in case an exception occurs before signing out and this exception was not catched, the user remains logged in.
An exception can occur when the mailserver is not available or the credentials are empty or invalid.
===============
I would let the admin create the user without any password. The email with link should go to the user. The user then is directed to SetPassword page to set new password. This way no one can access the user account unless he confirms and sets the password.
Call CreateAsync without the password
var adminresult = await UserManager.CreateAsync(user);
Redirect admin to new custom view saying something like "Email is sent to user"
#{
ViewBag.Title = "New User created and Email is Sent";
}
<h2>#ViewBag.Title.</h2>
<p class="text-info">
The New User has to follow the instructions to complete the user creation process.
</p>
<p class="text-danger">
Please change this code to register an email service in IdentityConfig to send an email.
</p>
The answer by #INFINITY_18 may cause Object reference not set to an instance of an object error if the email does not exist in the data store at all. And why not return the Login view with model error in this case, too?
I would suggest the following:
var userid = UserManager.FindByEmail(model.Email)?.Id;
if (string.IsNullOrEmpty(userid) || !UserManager.IsEmailConfirmed(userid)))
{
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
Require email confirmation
It's a best practice to confirm the email of a new user registration to verify they are not impersonating someone else (that is, they haven't registered with someone else's email). Suppose you had a discussion forum, and you wanted to prevent "yli#example.com" from registering as "nolivetto#contoso.com." Without email confirmation, "nolivetto#contoso.com" could get unwanted email from your app. Suppose the user accidentally registered as "ylo#example.com" and hadn't noticed the misspelling of "yli," they wouldn't be able to use password recovery because the app doesn't have their correct email. Email confirmation provides only limited protection from bots and doesn't provide protection from determined spammers who have many working email aliases they can use to register.
You generally want to prevent new users from posting any data to your web site before they have a confirmed email.
Update ConfigureServices to require a confirmed email:
​
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
services.Configure<AuthMessageSenderOptions>(Configuration);
}

Resources