I'm currently trying to update the user's email/username from a mobile app to a Web API project. I'm currently using oauth and token authentication. When updating the identity user, the user becomes unauthenticated because the username and access token are no longer valid. From what I have read, I must update the identity claims. This is what I have tried so far:
var identity = new ClaimsIdentity(User.Identity);
if (result)
{
var identityUser = await UserManager.FindByNameAsync(User.Identity.Name);
identityUser.Email = AntiXssEncoder.HtmlEncode(value.Email, true);
identityUser.UserName = AntiXssEncoder.HtmlEncode(value.Email, true);
var identityResult = await UserManager.UpdateAsync(identityUser);
if(identityResult.Succeeded)
{
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
await UserManager.RemoveClaimAsync(identityUser.Id, identity.FindFirst(ClaimTypes.Name));
await UserManager.AddClaimAsync(identityUser.Id, new Claim(ClaimTypes.Name, value.Email));
identity.RemoveClaim(identity.FindFirst(ClaimTypes.Name));
identity.AddClaim(new Claim(ClaimTypes.Name, value.Email));
authenticationManager.AuthenticationResponseGrant =
new AuthenticationResponseGrant(
new ClaimsPrincipal(identity),
new AuthenticationProperties { IsPersistent = false });
}
return Ok();
}
However, it still shows the previous email when using User.Identity.Name and the claims for the user within the authenticationManager have not been updated either. I'm not sure what else to do as there isn't much documentation on this for Web API. Any help is greatly appreciated.
Main problem is that claim which represents user's name is not updated in ClaimsIdentity you are using in the last step.
The easiest way to perform the update is to use SignInManager<TUser, TKey>.SignIn method
signInManager.SignIn(identityUser, isPersistent: false, rememberBrowser: false);
This is also an ASP.NET Identity idiomatic way since it is using associated IClaimsIdentityFactory to create claims for new identities.
Complete example
static async Task<IdentityResult> UpdateEmailAsync<TUser>(
IPrincipal principal,
UserManager<TUser, string> userManager,
SignInManager<TUser, string> signInManager,
string newEmail
)
where TUser : class, IUser<string>
{
string userId = principal.Identity.GetUserId();
IdentityResult result = await userManager.SetEmailAsync(userId, newEmail);
if (result.Succeeded)
{
// automatically confirm user's email
string confirmationToken = await userManager.GenerateEmailConfirmationTokenAsync(userId);
result = await userManager.ConfirmEmailAsync(userId, confirmationToken);
if (result.Succeeded)
{
TUser user = await userManager.FindByIdAsync(userId);
if (user != null)
{
// update username
user.UserName = newEmail;
await userManager.UpdateAsync(user);
// creates new identity with updated user's name
await signInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
// succeded
return result;
}
}
// failed
return result;
}
Then you can just call it from your code
string newEmail = AntiXssEncoder.HtmlEncode(value.Email, true);
IdentityResult result = await UpdateEmailAsync(identityUser, UserManager, SignInManager, newEmail);
if (result.Succeeded)
{
return Ok();
}
Related
I'm building C# (.net core 2.1) Razor Page site. Login mechanism will work based on documentation from here - so token in cookies (if I understand it correctly): https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-2.1
Let's say I have session set to 3 min, and if user is actively using site, I'm resetting this value. If he is not, 30 seconds before token expires I want to show him message like "Do something or You will be logged out".
My problem - I can't find way to get this "expires" time.
This is how I validate and store data:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
ReturnUrl = returnUrl;
int iCookieLifeTime = 10;
if (ModelState.IsValid)
{
var user = await AuthenticateUser(Input.UserName, Input.License, Input.Password, Input.KeepLogged);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim("Password", user.Password ) )
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(iCookieLifeTime)
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
Now, how can I call AuthenticationProperties to get/set data from ExpiresUtc on all my pages? Let's say that setting is working, because I defined parameter AllowRefresh = true. But getting is something I don't know how to do.
In ideal scenario, I'd like to get it on partial pages like I get my authentication data:
#inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor;
#if (HttpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
(...)
I'm using asp.net identiy to protect my api,
I use the following function to create Access Tokenfor users when they log in
private string GenerateAccessToken(string userName, string role)
{
ClaimsIdentity oAuthIdentity = new ClaimsIdentity(Startup.OAuthOptions.AuthenticationType);
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, userName));
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties());
DateTime currentUtc = DateTime.UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromDays(365));
string accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
Request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
return accessToken;
}
Everything is fine until I perform an account password update, after that I update SecurityStamp
UserManager.UpdateSecurityStampAsync(loggedinUser.Id);
but the problem is that the token can still be used to call my api without any problem. So how do I check SecurityStamp with each request?
You can check SecurityStamp using the JwtBearerEvents configured in startup.cs or program.cs depending of your .Net version.
This is a very simple version of SecurityStamp validation (.Net 6):
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg =>
{
cfg.Events = new JwtBearerEvents
{
OnTokenValidated = async (ctx) =>
{
var signInManager = ctx.HttpContext.RequestServices
.GetRequiredService<SignInManager<ApplicationUser>>();
var user = await signInManager.ValidateSecurityStampAsync(ctx.Principal);
if (user == null)
{
ctx.Fail("Invalid Security Stamp");
}
}
};
// more code...
});
Note:
For this example to work correctly, you need to ensure that you are packaging the SecurityStamp along with the user's claims during token creation, as in the example below.
var identityOptions = _config.Get<ClaimsIdentityOptions>();
claims.Add(new Claim(identityOptions.SecurityStampClaimType, user.SecurityStamp));
I have two websites using one database, I use asp.net identity (2.2.1.40403) and I have a problem I can't understand. Now, this is a third time this happened and I have no idea where the problem can be.
I have a register and send email method like this
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new User { UserName = model.Email, Email = model.Email, RegisterDate = DateTime.Now };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
//await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
await SendConfirmationEmail(user);
return View("ConfirmationEmailSent");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
private async Task SendConfirmationEmail(Dal.Models.User user)
{
// Send an email with this link
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Potvrzení Vašeho účtu", "Prosím potvrďte svou emailovou adresu kliknutím zde.");
}
What happened is that when user registered he received URL when userId was set to 3d847c51-7217-49fe-ae9d-d8e46e291559, but in database the user was created with 95789d6e-b66e-4c9e-8ee4-fe384b82e838. I don't understand how this can happen. By the way there is no user in database with Id 3d847c51-7217-49fe-ae9d-d8e46e291559. Do you have any idea why and how this can happen?
I would suggest calling back the user by an identifier after create was successful to make sure the properties match up.
//...other code removed for brevity
var user = new User { UserName = model.Email, Email = model.Email, RegisterDate = DateTime.Now };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
//pick one
//user = await UserManager.FindById(user.Id);
//user = await UserManager.FindByName(user.UserName);
user = await UserManager.FindByEmailAsync(user.Email);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
await SendConfirmationEmail(user);
return View("ConfirmationEmailSent");
}
AddErrors(result);
//...other code removed for brevity
I am also suspect that issue is related to UserManager.CreateAsync() method. You are using correctly. I will rather use manually generated user id instead generated by UserManager.
In your case will be:
var user = new User { UserName = model.Email, Email = model.Email, RegisterDate = DateTime.Now };
user.Id = Guid.NewGuid().ToString();
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SendConfirmationEmail(user);
return View("ConfirmationEmailSent");
}
I am trying to add a user to a role after successfully creating the user.
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (!ModelState.IsValid) return View(model);
var user = new ApplicationUser
{
UserName = model.PhoneNumber,
PhoneNumber = model.PhoneNumber,
NationalId = model.NationalId,
FullName = model.FullName
};
var result = await _userManager.CreateAsync(user, model.NationalId);
if (result.Succeeded)
{
var res = await _userManager.AddToRoleAsync(user, "Admin");
await _signInManager.SignInAsync(user, false);
_logger.LogInformation(3, "Applicant created a new account with password.");
return RedirectToLocal(returnUrl);
}
AddErrors(result);
// If we got this far, something failed, redisplay form
return View(model);
}
But, I get this error.
An unhandled exception occurred while processing the request.
InvalidOperationException: Role ADMIN does not exist.
Update:
I called the
var myrole = await _roleManager.FindByNameAsync("Admin");
and it returned null. but when i inspect
var roles = _roleManager.Roles
i get all the roles including "Admin"
I found the problem in the seed method. i do not understand it however.
in the seed method i used the RoleStore to add roles.
var roles = new[] {"Admin", "Applicant", "Student", "Role1", "Role2", "Role3", "Role4"};
foreach (var role in roles)
{
var roleStore = new RoleStore<IdentityRole>(context);
if (!context.Roles.Any(r => r.Name == role))
await roleStore.CreateAsync(new IdentityRole(role));
}
the roles where created successfully in the database table AspNetRoles.
but when acted upon, the roles were never found.
i replaced the RoleStore with RoleManager
await _roleManager.CreateAsync(new IdentityRole(role));
and like magic, it all worked out. i will do further research on the difference and the cause to understand it more.
I'm trying to lock user login after 3 unsuccessful login attempts for 5 minutes. I have add this 3 lines to App_Start/IdentityConfig.cs public static ApplicationUserManager Create( ... ) method:
manager.MaxFailedAccessAttemptsBeforeLockout = 3;
manager.DefaultAccountLockoutTimeSpan = new TimeSpan(0, 5, 0);
manager.UserLockoutEnabledByDefault = true;
After that I register new user via POST /api/Account/Register (in default scaffolded AccountController). Account is created and LockoutEnabled property is set to true. But if I try to login for via POST /Token few times with wrong password account isn't locked down.
I'm also interested where is implementation of /Token endpoint. Is it in AccountController GET api/Account/ExternalLogin. I have set breakpoint there but execution wasn't stopped there when I tried to login.
What am I missing?
If you are using the default Web API template from Visual Studio, you have to change the behavior of GrantResourceOwnerCredentials method of the ApplicationOAuthProvider class (found inside the Provider folder of your Web API project). Something like this could allow you to track failed login attempts, and stop locked out users from logging in:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
var user = await userManager.FindByNameAsync(context.UserName);
if (user == null)
{
context.SetError("invalid_grant", "Wrong username or password."); //user not found
return;
}
if (await userManager.IsLockedOutAsync(user.Id))
{
context.SetError("locked_out", "User is locked out");
return;
}
var check = await userManager.CheckPasswordAsync(user, context.Password);
if (!check)
{
await userManager.AccessFailedAsync(user.Id);
context.SetError("invalid_grant", "Wrong username or password."); //wrong password
return;
}
await userManager.ResetAccessFailedCountAsync(user.Id);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
Be aware that this way you can only lock out users trying to login using the password grant (Resource Owner Credentials). If you also want to disallow locked out user to login using other grants, you have to override the other methods (GrantAuthorizationCode, GrantRefreshToken, etc.), checking if await userManager.IsLockedOutAsync(user.Id) is true in those methods too.