Has ASP.NET Identity UserManager.CreateAsync() been updated with breaking changes recently? - c#

A few months ago I created my own implementation of ASP.NET Identity, overriding the UserStore to use dapper and a custom sql connection instead of Entity Framework. It worked fine at the time.
Now I updated all of the nuget packages today and I've been fighting problems since. Primarily when I register a new user by calling var result = await UserManager.CreateAsync(user, newAccount.Password); It creates the user and performs all other checks just fine, but then throws a bizarre error saying Invalid operation. The connection is closed.
Its as though UserManager.CreateAsync has a new method that needs to be overridden, but I have absolutely no idea what it could be.
For reference, here are parts of my implementation:
Account Controller:
[Authorize]
public class AccountController : Controller
{
public UserManager<User> UserManager { get; private set; }
public UserTokenProvider UserTokenProvider { get; set; }
public AccountController() : this(new UserManager<User>(new UserStore(ConfigurationManager.ConnectionStrings["DBConn"].ConnectionString)))
{
}
public AccountController(UserManager<User> userManager)
{
UserManager = userManager;
UserManager.PasswordHasher = new NoPasswordHasher();
}
...
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegistrationModel newAccount)
{
try
{
if (DbConfig.MaintenanceMode) return RedirectToAction("ComingSoon", "Home");
if (ModelState.IsValid)
{
var user = new User(newAccount);
var result = await UserManager.CreateAsync(user, newAccount.Password);
if (result.Succeeded)
{
await SignInAsync(user, isPersistent: false);
var userIn = await UserManager.FindByEmailAsync(newAccount.UserName);
if (!userIn.EmailConfirmed)
{
await SendValidationEmail(userIn);
return RedirectToAction("ConfirmationSent", new {userName = user.UserName});
}
return RedirectToAction("Index", "Home");
}
else
{
AddErrors(result);
}
}
// If we got this far, something failed, redisplay form
return View(newAccount);
}
catch (Exception ex)
{
var msg = ex.Message;
return View(newAccount);
}
}
User Store:
public class UserStore : IUserStore<User>, IUserLoginStore<User>, IUserPasswordStore<User>, IUserSecurityStampStore<User>, IUserRoleStore<User>, IUserEmailStore<User>
{
private readonly string _dbConn;
public UserStore(string conn = null)
{
if (conn != null)
_dbConn = conn;
else
_dbConn = DbConfig.ConnectionString;
}
public void Dispose()
{
}
public virtual Task CreateAsync(User user)
{
using (var _conn = new SqlConnection(_dbConn))
{
if (_conn.State == ConnectionState.Closed) _conn.Open();
return _conn.ExecuteAsync("users_UserCreate",
new
{
#UserId = user.Id,
#UserName = user.UserName,
#PasswordHash = user.PasswordHash,
#SecurityStamp = user.SecurityStamp
}, commandType: CommandType.StoredProcedure);
}
}
... Remaining methods omitted for brevity ...
You'll notice that the UserStore.CreateAsync() function has if (_conn.State == ConnectionState.Closed) _conn.Open(); as this was the suggestion from multiple threads regarding the connection closed error. Even without this line the query works fine and inserts a new user into the database correctly.
The error is coming from somewhere after UserStore.CreateAsync() is called by UserManager.CreateAsync().
Any idea what's missing?

The answer is no, ASP.NET Identity hasn't altered with breaking changes.
Using DotNetPeek I had a look at the Identity library to see what methods were being called during UserManager.CreateAsync() and it only calls UserStore.CreateSync and a password update.
After playing around with the code some more it dawned on me that whilst UserManager.CreateSync is awaited, the internal calls to UserStore.CreateSync isn't. Tanks to having to override public virtual Task CreateAsync(User user) and having to return a task that isn't awaited, we have to juggle some code to await the response from Dapper before returning it as part of a task again.
So, here's the updated UserStore.CreateAsync override that works. Note: if (_conn.State == ConnectionState.Closed) _conn.Open(); isn't actually needed as the connection was being closed before the method had finished, Dapper does a brilliant job of handling connections for you.
public virtual Task CreateAsync(User user)
{
using (var _conn = new SqlConnection(_dbConn))
{
var result = _conn.ExecuteAsync("users_UserCreate", new
{
#UserId = user.Id,
#UserName = user.UserName,
#PasswordHash = user.PasswordHash,
#SecurityStamp = user.SecurityStamp
}, commandType: CommandType.StoredProcedure).ConfigureAwait(true);
return Task.FromResult(result);
}
}
Hopefully this will help someone else in the future facing the same sort of problem.

Related

When user table gets a new record, create record in another table with user ID in .NET Core identity

I have to code, When user table gets a new record, then automatically create record in siteUSer table with userID and siteCodeId in .net core.
user table does not have siteCodeId as a column. I need to add a record of userId with corresponding siteCodeID into siteUSer table.
public class SiteUsers
{
public int SiteCodeId { get; set; }
public SiteCode SiteCode { get; set; }
public string UserId { get; set; }
public User User { get; set; }
}
This is usersController.cs:
[HttpPost]
public async Task<IActionResult> Create(UserBO userBO)
{
try
{
await _userService.CreateUserAsync(userBO);
return Created(nameof(Get), userBO);
}
catch (Exception ex)
{
return HandleException(ex);
}
}
This is usersService.cs:
public async Task<UserBO> CreateUserAsync(UserBO userBO)
{
var user = new User
{
UserName = userBO.UserName,
Email = userBO.Email,
EmailConfirmed = true,
RecordState = Enums.RecordState.Active,
};
var result = await _userManager.CreateAsync(user, userBO.Password);
if (userBO.Roles.Count > 0)
{
// superadmin users can be created manually
userBO.Roles = userBO.Roles.Where(i => i != "SuperAdmin").ToList();
}
foreach (var item in userBO.Roles)
{
await _userManager.AddToRoleAsync(user, item);
}
return userBO;
}
You are performing three operations:
Create a new user
Add the user to some roles
Create a SiteUsers for the user
Add the following methods for each of these operations in UserService.cs.
Inject the DbContext(assuming you are using EF Core; if not, inject the equivalent service to add SiteUsers to the database). Use this service to add a new SiteUsers to the database.
Also, you can use the email from the UserBO to look up an already created user.
You don't even need to use a for loop to add a user to multiple roles, you can use AddToRolesAsync() that takes in an IEnumerable<string>:
public async Task<IdentityResult> CreateUserAsync(UserBO userBO)
{
var user = new User
{
UserName = userBO.UserName,
Email = userBO.Email,
EmailConfirmed = true,
RecordState = Enums.RecordState.Active,
};
return await _userManager.CreateAsync(user, userBO.Password);
}
public async Task CreateSiteUsersAsync(UserBO userBO, User user)
{
SiteUsers siteUsers = new SiteUsers { UserId = user.Id, User = user };
await _context.AddAsync(siteUsers);
await _context.SaveChangesAsync();
}
public async Task<IdentityResult> AddToRolesAsync(UserBO userBO, User user)
{
if (userBO.Roles.Count > 0)
{
userBO.Roles = userBO.Roles.Where(i => i != "SuperAdmin").ToList();
}
return await _userManager.AddToRolesAsync(user, userBO.Roles);
}
public async Task<User> FindByEmailAsync(string email) => _userManager.FindByEmailAsync(email);
Now, in your controller action, call each method:
public async Task<IActionResult> Create(UserBO userBO)
{
try
{
//Create user
IdentityResult createUserResult = await _userService.CreateUserAsync(userBO);
if(!createUserResult.Succeeded)
{
//Handle error
}
//Find created user
User user = await _userService.FindByEmailAsync(userBO.Email);
if(user is null)
{
//Handle error
}
//Add to roles
IdentityResult addToRolesResult = await _userService.AddToRolesAsync(userBO, user);
if(!addToResult.Succeeded)
{
//Handle error
}
await CreateSiteUsersAsync(userBO, user)
return Created(nameof(Get), userBO);
}
catch (Exception ex)
{
return HandleException(ex);
}
}
Please try the following code , it should work
var userName=UserBPO.email;
var user=_userManager.FindByNameAsync(userName);// You can use FindByEmailAsync as well
if(user!=null)
{
//Assign the role and populate SiteUsers
//Save SiteCode to database and get its ID in siteId
SiteUsers siteUsers = new SiteUsers { UserId = user.Id, SiteCodeId=siteId };
await _context.AddAsync(siteUsers);
}
You can try something like this, which would also fix the problem of getting the user id from the web request context.
Create an API service SiteUserService, that allows you to add site users.
Inject the data context and HTTP context.
Create a method CreateSiteUser() that takes the site code id and generates the SiteUser record.
Call API service SiteUserService method CreateSiteUser() from the web client.
The CreateSiteUser() method can be included within an existing or new controller method.
The service class SiteUserService is shown below:
public class SiteUserService
{
private readonly HttpContext _context;
private ApplicationDbContext _db;
public SiteUserService(IHttpContextAccessor httpContextAccessor,
ApplicationDbContext db)
{
_context = httpContextAccessor.HttpContext;
_db = db;
}
public async Task CreateSiteUser(int siteCodeID)
{
var siteUser = new SiteUsers
{
SiteCodeId = siteCodeID,
SiteCode = new SiteCode()
{
// set your site code properties in here..
},
UserId = _context.Request.HttpContext.User.Id,
User = new User()
{
// set your user properties in here
// e.g. _context.Request.HttpContext.User.Identity.Name
}
};
_db.Add(siteUser);
await _db.SaveChangesAsync();
}
...
}
The SiteCode and User object properties can be populated as needed.

How to Fix: Cannot access a disposed object. in ef-code-first and Core 3.1

I want to Save User Information In the Database.
public async Task<User> CreateAsync(CreateRequestViewModel user)
{
User NewUser = new User
{
UserId = Guid.NewGuid(),
Password = user.Password,
UserName = user.UserName,
};
var resultUser = await _ProjectContext.Users.AddAsync(NewUser);
Person person = new Person()
{
UserId = resultUser.Entity.UserId,
RegisterDate = DateTime.Now,
Email = user.Email,
Gender = user.Gender,
PersonId = Guid.NewGuid(),
};
await _ProjectContext.Persons.AddAsync(person);
return resultUser.Entity;
}
User Service Code:
public async Task<User> Create(CreateRequestViewModel user)
{
try
{
var model = await _unitOfWork.Users.CreateAsync(user);
await _unitOfWork.CommitAsync();
return model;
}
catch (Exception me)
{
throw;
}
}
but when to run this code:
await _unitOfWork.CommitAsync();
code in CommitAsync:
public async Task<int> CommitAsync()
{
int oo = 0;
try
{
oo = await _context.SaveChangesAsync();
}
catch (Exception me)
{
throw;
}
return oo;
}
I get this error:
Cannot access a disposed object. A common cause of this error is
disposing a context that was resolved from dependency injection and
then later trying to use the same context instance elsewhere in your
application. This may occur if you are calling Dispose() on the
context, or wrapping the context in a using statement. If you are
using dependency injection, you should let the dependency injection
container take care of disposing context instances. Object name:
'ProjectContext'.
How it can be fixed.

Invalid Token using GenerateEmailConfirmationTokenAsync Outside Controller in MVC

I've been stuck on this for days.
I am using GenerateEmailConfirmationTokenAsync to create a token outside the Controller (it's working fine), but somehow my token is longer than the ones created within the Controller using the GenerateEmailConfirmationTokenAsync and therefore the ConfirmEmail action rejects the token. (Error: Invalid Token).
I have tried Machinekey on web.config, HttpUtility.UrlEncode, but I am still stuck.
How to sort out the Invalid Token error on Controller ConfirmEmail?
Here is my Code:
RegisterUser (outside Controller)
public async Task RegisterUserAsync()
{
var store = new UserStore<ApplicationUser>(db);
var UserManager = new ApplicationUserManager(store);
var query = from c in db.Customer
where !(from o in db.Users
select o.customer_pk)
.Contains(c.customer_pk)
select c;
var model = query.ToList();
if (query != null)
{
foreach (var item in model)
{
var user = new ApplicationUser { UserName = item.email, Email = item.email, customerId = item.customerId};
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id);
SmtpClient client = new SmtpClient();
MailMessage message = new MailMessage
{
IsBodyHtml = true
};
message.Subject = "Confirm Email";
message.To.Add(item.email1);
message.Body = "Please confirm your account by clicking here";
client.SendAsync(message, "userToken");
//Assign Role User Here
await UserManager.AddToRoleAsync(user.Id, "Client");
}
}
}
}
SendEmailConfirmation method (outside Controller)
public async Task<string> SendEmailConfirmationTokenAsync(string userID)
{
var store = new UserStore<ApplicationUser>(db);
var UserManager = new ApplicationUserManager(store);
var url = new UrlHelper();
var provider = new DpapiDataProtectionProvider("MyApp");
UserManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
provider.Create("EmailConfirmation"));
string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
string encodedCode = HttpUtility.UrlEncode(code);
string callbackUrl = "http://localhost/Accounts/ConfirmEmail?userId=" + userID + "&code=" + encodedCode;
return callbackUrl;
}
where db is
ApplicationdDbContext db = new ApplicationdDbContext();
ConfirmEmail within the Identity Controller (Accounts Controller) - I've created Accounts instead of Account controller but it's working fine.
//
// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var confirmed = await UserManager.IsEmailConfirmedAsync(userId);
if (confirmed)
{
return RedirectToLocal(userId);
}
var result = await UserManager.ConfirmEmailAsync(userId, code); //Here I get the error (Token Invlaid, despite the token and userId being displayed)
if (result.Succeeded)
{
ViewBag.userId = userId;
ViewBag.code = code;
}
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(SetPasswordViewModel model, string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
if (!ModelState.IsValid)
{
return View(model);
}
var result = await UserManager.AddPasswordAsync(userId, model.NewPassword);
if (result.Succeeded)
{
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
return RedirectToLocal(userId);
}
ViewBag.userId = userId;
ViewBag.code = code;
AddErrors(result);
return View(model);
}
I have worked for hours in this code but until now I can't sort it out.
Thanks for any comments or solution. The reason for this approach is that I have to use task scheduler (I'm using fluentscheduler, which is working fine).
Your problem is in this line:
var provider = new DpapiDataProtectionProvider("MyApp");
UserManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
provider.Create("EmailConfirmation"));
DpapiDataProtectionProvider here is just not the same as what Identity uses while running under IIS. As far as I remember instead of "MyApp" it uses internal name for IIS web-site. Also it does some magic with registering it through delegates and as a singleton.
You can try to save a static reference to data protection provider and use it in your scheduler code. In Startup.Auth.cs class do this:
public partial class Startup
{
internal static IDataProtectionProvider DataProtectionProvider { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
DataProtectionProvider = app.GetDataProtectionProvider();
// other stuff.
}
}
Then in your UserManager access for that reference like this:
public class UserManager : UserManager<ApplicationUser>
{
public UserManager() : base(new UserStore<ApplicationUser>(new MyDbContext()))
{
var dataProtectionProvider = Startup.DataProtectionProvider;
this.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
// do other configuration
}
}
However I'm not familiar wiht details of FluentScheduler and it might not let you access this static variable if it starts processes in separate AppDomain. But give it a try and see how it works.

Registering and update model in a single API call

I have an ASP.net API 2 application with a register method, which basically does the following:
var user = new ApplicationUser() { UserName = user.email, Email = user.email };
UserManager.AddToRole(user.Id, "SomeRole");
In the same controller, I would like to assign a model class to the application user, for example:
var parent = db.Parents.Find(parentToCreate.id);
db.SaveChanges();
This gives me an error that the user name is already taken. I know that the issue is relating to there being two model trackers, one for the UserManager.CreateAsync and one for updating the db. Will it be alright to create users without using CreateAsync or is there another way to avoid this error?
Note that I think that this could be achieved by putting a Parent property on the account property, but not all accounts are parents, so I do not want to do this solution. A parent has an account, so there is an account property on the parent.
As requested, the full register method is as follows:
[AllowAnonymous]
[Route("RegisterParent")]
public async Task<IHttpActionResult>RegisterParent(ParentRegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser() { UserName = model.email, Email = model.email };
Parent parentToCreate = new Parent();
db.Parents.Add(parentToCreate);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.SaveChanges();
try
{
IdentityResult result = await UserManager.CreateAsync(user, model.password);
// The following two lines give an error
parentToCreate.account = user;
// the above two lines give an error
UserManager.AddToRole(user.Id, "ParentRole");
db.SaveChanges();
}
catch (Exception e)
{
Console.Write(e);
// e returns here with message
}
return Ok(200);
}
This is a simplified example based on minimal example provided in OP.
Based on conversation to clarify current design the Parent model would need to be updated to have a proper code first Foreign Key relationship
public class Parent {
//...other properties
//Foreign key
public string accountid { get; set; }
//navigation property
[ForeignKey("accountid")]
public ApplicationUser account { get; set; }
}
With that then you only need to assign the user id when creating the parent.
Refactoring/abstracting out specific responsibilities.
public interface IParentService {
Task AddParentAsync(ApplicationUser user);
}
public class ParentService : IParentService {
ApplicationDbContext db;
public ParentService(ApplicationDbContext db) {
this.db = db;
}
public async Task AddParentAsync(ApplicationUser user) {
Parent parentToCreate = new Parent() {
//...set other properties
accountid = user.Id
};
db.Parents.Add(parentToCreate);
await db.SaveChangesAsync();
}
}
Next separating the action into distinct processes to avoid concurrency issues.
public class AccountController : ApiController {
ApplicationUserManager userManager;
IParentService parentService;
public AccountController(ApplicationUserManager userManager, IParentService parentService) {
this.userManager = userManager;
this.parentService = parentService;
}
[AllowAnonymous]
[Route("RegisterParent")]
public async Task<IHttpActionResult> RegisterParent(ParentRegisterBindingModel model) {
if (!ModelState.IsValid) {
return BadRequest(ModelState);
}
var user = new ApplicationUser() { UserName = model.email, Email = model.email };
var result = await userManager.CreateAsync(user, model.password);
if (result.Succeed) {
try {
await userManager.AddToRole(user.Id, "ParentRole");
await parentService.AddParentAsync(user);
return Ok();
} catch (Exception e) {
userManager.Delete(user.Id);
Console.Write(e);
// e returns here with message
return BadRequest(); //OR InternalServerError();
}
} else {
foreach (var error in result.Errors) {
ModelState.AddModelError("", error);
}
return BadRequest(ModelState);
}
}
}
You would obviously register dependencies with the DI framework to allow for proper injection.

Asp.NET Identity 2 giving "Invalid Token" error

I'm using Asp.Net-Identity-2 and I'm trying to verify email verification code using the below method. But I am getting an "Invalid Token" error message.
My Application's User Manager is like this:
public class AppUserManager : UserManager<AppUser>
{
public AppUserManager(IUserStore<AppUser> store) : base(store) { }
public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
{
AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
manager.PasswordValidator = new PasswordValidator {
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = true,
RequireUppercase = true
};
manager.UserValidator = new UserValidator<AppUser>(manager)
{
AllowOnlyAlphanumericUserNames = true,
RequireUniqueEmail = true
};
var dataProtectionProvider = options.DataProtectionProvider;
//token life span is 3 hours
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<AppUser>
(dataProtectionProvider.Create("ConfirmationToken"))
{
TokenLifespan = TimeSpan.FromHours(3)
};
}
manager.EmailService = new EmailService();
return manager;
} //Create
} //class
} //namespace
My Action to generate the token is (and even if I check the token here, I get "Invalid token" message):
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ForgotPassword(string email)
{
if (ModelState.IsValid)
{
AppUser user = UserManager.FindByEmail(email);
if (user == null || !(UserManager.IsEmailConfirmed(user.Id)))
{
// Returning without warning anything wrong...
return View("../Home/Index");
} //if
string code = UserManager.GeneratePasswordResetToken(user.Id);
string callbackUrl = Url.Action("ResetPassword", "Admin", new { Id = user.Id, code = HttpUtility.UrlEncode(code) }, protocol: Request.Url.Scheme);
UserManager.SendEmail(user.Id, "Reset password Link", "Use the following link to reset your password: link");
//This 2 lines I use tho debugger propose. The result is: "Invalid token" (???)
IdentityResult result;
result = UserManager.ConfirmEmail(user.Id, code);
}
// If we got this far, something failed, redisplay form
return View();
} //ForgotPassword
My Action to check the token is (here, I always get "Invalid Token" when I check the result):
[AllowAnonymous]
public async Task<ActionResult> ResetPassword(string id, string code)
{
if (id == null || code == null)
{
return View("Error", new string[] { "Invalid params to reset password." });
}
IdentityResult result;
try
{
result = await UserManager.ConfirmEmailAsync(id, code);
}
catch (InvalidOperationException ioe)
{
// ConfirmEmailAsync throws when the id is not found.
return View("Error", new string[] { "Error to reset password:<br/><br/><li>" + ioe.Message + "</li>" });
}
if (result.Succeeded)
{
AppUser objUser = await UserManager.FindByIdAsync(id);
ResetPasswordModel model = new ResetPasswordModel();
model.Id = objUser.Id;
model.Name = objUser.UserName;
model.Email = objUser.Email;
return View(model);
}
// If we got this far, something failed.
string strErrorMsg = "";
foreach(string strError in result.Errors)
{
strErrorMsg += "<li>" + strError + "</li>";
} //foreach
return View("Error", new string[] { strErrorMsg });
} //ForgotPasswordConfirmation
I don't know what could be missing or what's wrong...
I encountered this problem and resolved it. There are several possible reasons.
1. URL-Encoding issues (if problem occurring "randomly")
If this happens randomly, you might be running into url-encoding problems.
For unknown reasons, the token is not designed for url-safe, which means it might contain invalid characters when being passed through a url (for example, if sent via an e-mail).
In this case, HttpUtility.UrlEncode(token) and HttpUtility.UrlDecode(token) should be used.
As oão Pereira said in his comments, UrlDecode is not (or sometimes not?) required. Try both please. Thanks.
2. Non-matching methods (email vs password tokens)
For example:
var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);
and
var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
The token generated by the email-token-provide cannot be confirmed by the reset-password-token-provider.
But we will see the root cause of why this happens.
3. Different instances of token providers
Even if you are using:
var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
along with
var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
the error still could happen.
My old code shows why:
public class AccountController : Controller
{
private readonly UserManager _userManager = UserManager.CreateUserManager();
[AllowAnonymous]
[HttpPost]
public async Task<ActionResult> ForgotPassword(FormCollection collection)
{
var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action("ResetPassword", "Account", new { area = "", UserId = user.Id, token = HttpUtility.UrlEncode(token) }, Request.Url.Scheme);
Mail.Send(...);
}
and:
public class UserManager : UserManager<IdentityUser>
{
private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
private static readonly UserManager Instance = new UserManager();
private UserManager()
: base(UserStore)
{
}
public static UserManager CreateUserManager()
{
var dataProtectionProvider = new DpapiDataProtectionProvider();
Instance.UserTokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());
return Instance;
}
Pay attention that in this code, every time when a UserManager is created (or new-ed), a new dataProtectionProvider is generated as well. So when a user receives the email and clicks the link:
public class AccountController : Controller
{
private readonly UserManager _userManager = UserManager.CreateUserManager();
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(string userId, string token, FormCollection collection)
{
var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
if (result != IdentityResult.Success)
return Content(result.Errors.Aggregate("", (current, error) => current + error + "\r\n"));
return RedirectToAction("Login");
}
The AccountController is no longer the old one, and neither are the _userManager and its token provider. So the new token provider will fail because it has no that token in it's memory.
Thus we need to use a single instance for the token provider. Here is my new code and it works fine:
public class UserManager : UserManager<IdentityUser>
{
private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
private static readonly UserManager Instance = new UserManager();
private UserManager()
: base(UserStore)
{
}
public static UserManager CreateUserManager()
{
//...
Instance.UserTokenProvider = TokenProvider.Provider;
return Instance;
}
and:
public static class TokenProvider
{
[UsedImplicitly] private static DataProtectorTokenProvider<IdentityUser> _tokenProvider;
public static DataProtectorTokenProvider<IdentityUser> Provider
{
get
{
if (_tokenProvider != null)
return _tokenProvider;
var dataProtectionProvider = new DpapiDataProtectionProvider();
_tokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());
return _tokenProvider;
}
}
}
It could not be called an elegant solution, but it hit the root and solved my problem.
Because you are generating token for password reset here:
string code = UserManager.GeneratePasswordResetToken(user.Id);
But actually trying to validate token for email:
result = await UserManager.ConfirmEmailAsync(id, code);
These are 2 different tokens.
In your question you say that you are trying to verify email, but your code is for password reset. Which one are you doing?
If you need email confirmation, then generate token via
var emailConfirmationCode = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
and confirm it via
var confirmResult = await UserManager.ConfirmEmailAsync(userId, code);
If you need password reset, generate token like this:
var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
and confirm it like this:
var resetResult = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
I was getting the "Invalid Token" error even with code like this:
var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);
In my case the problem turned out to be that I was creating the user manually and adding him to the database without using the UserManager.Create(...) method. The user existed in the database but without a security stamp.
It's interesting that the GenerateEmailConfirmationToken returned a token without complaining about the lack of security stamp, but that token could never be validated.
Other than that, I've seen the code itself fail if it's not encoded.
I've recently started encoding mine in the following fashion:
string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
And then when I'm ready to read it back:
string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);
To be quite honest, I'm surprised that it isn't being properly encoded in the first place.
In my case, our AngularJS app converted all plus signs (+) to empty spaces (" ") so the token was indeed invalid when it was passed back.
To resolve the issue, in our ResetPassword method in the AccountController, I simply added a replace prior to updating the password:
code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);
I hope this helps anyone else working with Identity in a Web API and AngularJS.
tl;dr: Register custom token provider in aspnet core 2.2 to use AES encryption instead of MachineKey protection, gist: https://gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b
i ran into the same issue with aspnet core 2.2, as cheny pointed out the instances of the token provider needs to be the same.
this does not work for me because
i got different API-projects which does generate the token and
receive the token to reset password
the APIs may run on different instances of virtual machines, so the machine key would not be the
same
the API may restart and the token would be invalid because it is
not the same instance any more
i could use
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path"))
to save the token to the file system and avoid restart and multiple instance sharing issues, but could not get around the issue with multiple projects, as each project generates a own file.
the solution for me is to replace the MachineKey data protection logic with an own logic which does use AES then HMAC to symmetric encrypt the token with a key from my own settings which i can share across machines, instances and projects. I took the encryption logic from
Encrypt and decrypt a string in C#?
(Gist: https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs)
and implemented a custom TokenProvider:
public class AesDataProtectorTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
{
public AesDataProtectorTokenProvider(IOptions<DataProtectionTokenProviderOptions> options, ISettingSupplier settingSupplier)
: base(new AesProtectionProvider(settingSupplier.Supply()), options)
{
var settingsLifetime = settingSupplier.Supply().Encryption.PasswordResetLifetime;
if (settingsLifetime.TotalSeconds > 1)
{
Options.TokenLifespan = settingsLifetime;
}
}
}
public class AesProtectionProvider : IDataProtectionProvider
{
private readonly SystemSettings _settings;
public AesProtectionProvider(SystemSettings settings)
{
_settings = settings;
if(string.IsNullOrEmpty(_settings.Encryption.AESPasswordResetKey))
throw new ArgumentNullException("AESPasswordResetKey must be set");
}
public IDataProtector CreateProtector(string purpose)
{
return new AesDataProtector(purpose, _settings.Encryption.AESPasswordResetKey);
}
}
public class AesDataProtector : IDataProtector
{
private readonly string _purpose;
private readonly SymmetricSecurityKey _key;
private readonly Encoding _encoding = Encoding.UTF8;
public AesDataProtector(string purpose, string key)
{
_purpose = purpose;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
}
public byte[] Protect(byte[] userData)
{
return AESThenHMAC.SimpleEncryptWithPassword(userData, _encoding.GetString(_key.Key));
}
public byte[] Unprotect(byte[] protectedData)
{
return AESThenHMAC.SimpleDecryptWithPassword(protectedData, _encoding.GetString(_key.Key));
}
public IDataProtector CreateProtector(string purpose)
{
throw new NotSupportedException();
}
}
and the SettingsSupplier i use in my project to supply my settings
public interface ISettingSupplier
{
SystemSettings Supply();
}
public class SettingSupplier : ISettingSupplier
{
private IConfiguration Configuration { get; }
public SettingSupplier(IConfiguration configuration)
{
Configuration = configuration;
}
public SystemSettings Supply()
{
var settings = new SystemSettings();
Configuration.Bind("SystemSettings", settings);
return settings;
}
}
public class SystemSettings
{
public EncryptionSettings Encryption { get; set; } = new EncryptionSettings();
}
public class EncryptionSettings
{
public string AESPasswordResetKey { get; set; }
public TimeSpan PasswordResetLifetime { get; set; } = new TimeSpan(3, 0, 0, 0);
}
finally register the provider in Startup:
services
.AddIdentity<AppUser, AppRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders()
.AddTokenProvider<AesDataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider);
services.AddScoped(typeof(ISettingSupplier), typeof(SettingSupplier));
//AESThenHMAC.cs: See https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs
string code = _userManager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
//send rest email
do not decode the code
var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
Here is what I did: Decode Token after encoding it for URL (in short)
First I had to Encode the User GenerateEmailConfirmationToken that was generated. (Standard above advice)
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var encodedToken = HttpUtility.UrlEncode(token);
and in your controller's "Confirm" Action I had to decode the Token before I validated it.
var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
var result = await userManager.ConfirmEmailAsync(user,decodedCode);
Hit this issue with asp.net core and after a lot of digging I realised I'd turned this option on in Startup:
services.Configure<RouteOptions>(options =>
{
options.LowercaseQueryStrings = true;
});
This of course invalidated the token that was in the query string.
Here I've the same problem but after a lot of time I found that in my case the invalid token error was raised by the fact that my custom Account class has the Id property re-declared and overridden.
Like that:
public class Account : IdentityUser
{
[ScaffoldColumn(false)]
public override string Id { get; set; }
//Other properties ....
}
So to fix it I've just removed that property and generated again the database schema just to be sure.
Removing this solves the problem.
The following solution helped me in WebApi:
Registration
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded) {
EmailService emailService = new EmailService();
var url = _configuration["ServiceName"];
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var encodedToken = HttpUtility.UrlEncode(token);
// .Net Core 2.1, Url.Action return null
// Url.Action("confirm", "account", new { userId = user.Id, code = token }, protocol: HttpContext.Request.Scheme);
var callbackUrl = _configuration["ServiceAddress"] + $"/account/confirm?userId={user.Id}&code={encodedToken}";
var message = emailService.GetRegisterMailTemplate(callbackUrl, url);
await emailService.SendEmailAsync( model.Email, $"please confirm your registration {url}", message );
}
Confirm
[Route("account/confirm")]
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> ConfirmEmail(string userId, string code) {
if (userId == null || code == null) {
return Content(JsonConvert.SerializeObject( new { result = "false", message = "data is incorrect" }), "application/json");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null) {
return Content(JsonConvert.SerializeObject(new { result = "false", message = "user not found" }), "application/json");
}
//var decodedCode = HttpUtility.UrlDecode(code);
//var result = await _userManager.ConfirmEmailAsync(user, decodedCode);
var result = await _userManager.ConfirmEmailAsync(user, code);
if (result.Succeeded)
return Content(JsonConvert.SerializeObject(new { result = "true", message = "ок", token = code }), "application/json");
else
return Content(JsonConvert.SerializeObject(new { result = "false", message = "confirm error" }), "application/json");
}
Insipired by the soluion #3 posted by #cheny, I realized that if you use the same UserManager instance the generated code is accepted. But in a real scenario, the validation code happens in a second API call after the user clicks on the email link.
It means that a new instance of the UserManager is created and it is unable to verify the code generated by the first instance of the first call. The only way to make it work is to be sure to have the SecurityStamp column in the database user table.
Registering the class that's using the UserManager as singleton throws an exception at the application startup because the UserManager class is automatically registered with a Scoped lifetime
Make sure when generate, you use:
GeneratePasswordResetTokenAsync(user.Id)
And confirm you use:
ResetPasswordAsync(user.Id, model.Code, model.Password)
If you make sure you are using the matching methods, but it still doesn't work, please verify that user.Id is the same in both methods. (Sometimes your logic may not be correct because you allow using same email for registry, etc.)
Maybe this is an old thread but, just for the case, I've been scratching my head with the random occurrence of this error. I've been checking all threads about and verifying each suggestion but -randomly seemed- some of the codes where returned as "invalid token".
After some queries to the user database I've finally found that those "invalid token" errors where directly related with spaces or other non alphanumerical characters in user names.
Solution was easy to find then. Just configure the UserManager to allow those characters in user's names.
This can be done just after the user manager create event, adding a new UserValidator setting to false the corresponding property this way:
public static UserManager<User> Create(IdentityFactoryOptions<UserManager<User>> options, IOwinContext context)
{
var userManager = new UserManager<User>(new UserStore());
// this is the key
userManager.UserValidator = new UserValidator<User>(userManager) { AllowOnlyAlphanumericUserNames = false };
// other settings here
userManager.UserLockoutEnabledByDefault = true;
userManager.MaxFailedAccessAttemptsBeforeLockout = 5;
userManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(1);
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
userManager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = TimeSpan.FromDays(5)
};
}
return userManager;
}
Hope this could help "late arrivals" like me!
Make sure that the token that you generate doesn't expire rapidly - I had changed it to 10 seconds for testing and it would always return the error.
if (dataProtectionProvider != null) {
manager.UserTokenProvider =
new DataProtectorTokenProvider<AppUser>
(dataProtectionProvider.Create("ConfirmationToken")) {
TokenLifespan = TimeSpan.FromHours(3)
//TokenLifespan = TimeSpan.FromSeconds(10);
};
}
We have run into this situation with a set of users where it was all working fine. We have isolated it down to Symantec's email protection system which replaces links in our emails to users with safe links that go to their site for validation and then redirects the user to the original link we sent.
The problem is that they are introducing a decode... they appear to do a URL Encode on the generated link to embed our link as a query parameter to their site but then when the user clicks and clicksafe.symantec.com decodes the url it decodes the first part they needed to encode but also the content of our query string and then the URL that the browser gets redirected to has been decoded and we are back in the state where the special characters mess up the query string handling in the code behind.
In my case, I just need to do HttpUtility.UrlEncode before sending an email. No HttpUtility.UrlDecode during reset.
Related to chenny's 3. Different instances of token providers .
In my case I was passing IDataProtectionProvider.Create a new guid every time it got called, which prevented existing codes from being recognized in subsequent web api calls (each request creates its own user manager).
Making the string static solved it for me.
private static string m_tokenProviderId = "MyApp_" + Guid.NewGuid().ToString();
...
manager.UserTokenProvider =
new DataProtectorTokenProvider<User>(
dataProtectionProvider.Create(new string[1] { m_tokenProviderId } ))
{
TokenLifespan = TimeSpan.FromMinutes(accessTokenLifespan)
};
In case anyone runs into this, it turns out that the token was not URL-friendly, and so I had to wrap it in a HttpUtility.UrlEncode() like so:
var callback = Url.Content($"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}/reset-password?token={HttpUtility.UrlEncode(token)}&email={user.Email}");
I have solved "Invalid Token" issue most of described hints. Here is my solution for blazor project. The core is in StringExtensions class.
Generating email when user is registering his/her email:
user = new IdentityUser { UserName = email, Email = email };
var createUser = await _userManager.CreateAsync(user, password);
if (createUser.Succeeded)
{
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var baseUri = NavMgr.BaseUri;
var setNewPasswordUri = baseUri + "confirm-password";
var urlWithParams = StringExtensions.GenerateUrl(token, emailTo, url);
await SendAsync( urlWithParams ); // use your own Email solution send the email
}
Email confirmation (user clicks on the link in the mail)
#page "/confirm-email"
<h3>Confirm email</h3>
#Error
[Inject]
UserManager<IdentityUser> UserMgr { get; set; }
[Inject]
NavigationManager NavMgr { get; set; }
protected override Task OnInitializedAsync()
{
var url = NavMgr.Uri;
Token = StringExtensions.GetParamFromUrl(url, "token");
Email = StringExtensions.GetParamFromUrl(url, "email");
log.Trace($"Initialised with email={Email} , token={Token}");
return ActivateEmailAsync();
}
private async Task ActivateEmailAsync()
{
isProcessing = true;
Error = null;
log.Trace($"ActivateEmailAsync started for {Email}");
isProcessing = true;
Error = null;
try
{
var user = await UserMgr.FindByEmailAsync(Email);
if (user != null)
{
if (!string.IsNullOrEmpty(Token))
{
var result = await UserMgr.ConfirmEmailAsync(user, Token);
if (result.Succeeded)
{
// Show user , that account is activated
}
else
{
foreach (var error in result.Errors)
{
Error += error.Description;
}
log.Error($"Setting new password failed for {Email} due to the: {Error}");
}
}
else
{
log.Error("This should not happen. Token is null or empty");
}
}
}
catch (Exception exc)
{
Error = $"Activation failed";
}
isProcessing = false;
}
public static class StringExtensions
{
/// <summary>
/// Encode string to be safe to use it in the URL param
/// </summary>
/// <param name="toBeEncoded"></param>
/// <returns></returns>
public static string Encode(string toBeEncoded)
{
var result = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(toBeEncoded));
return result;
}
/// <summary>
/// Decode from the url safe string the original value
/// </summary>
/// <param name="toBeDecoded"></param>
/// <returns></returns>
public static string Decode(string toBeDecoded)
{
var decodedBytes = WebEncoders.Base64UrlDecode(toBeDecoded);
var result = Encoding.UTF8.GetString(decodedBytes);
return result;
}
public static string GenerateUrl(string token, string emailTo, string baseUri, string tokenParamName = "token", string emailParamName = "email")
{
var tokenEncoded = StringExtensions.Encode(token);
var emailEncoded = StringExtensions.Encode(emailTo);
var queryParams = new Dictionary<string, string>();
queryParams.Add(tokenParamName, tokenEncoded);
queryParams.Add(emailParamName, emailEncoded);
var urlWithParams = QueryHelpers.AddQueryString(baseUri, queryParams);
return urlWithParams;
}
public static string GetParamFromUrl(string uriWithParams, string paramName)
{
var uri = new Uri(uriWithParams, UriKind.Absolute);
var result = string.Empty;
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue(paramName, out var paramToken))
{
var queryToken = paramToken.First();
result = StringExtensions.Decode(queryToken);
}
return result;
}
I have experienced Invalid token in Reset password scenario. The root cause was, that I was generating reset token for for incorrect IndentityUser. It can be spotted easily in simplified code, but it it took me some to fix it time in more complex code.
I should have used the code:
var user = await UserMgr.FindByEmailAsync(Model.Email);
string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
But I was wrongly ( creating another IndentityUser).
// This is example "How it should not be done"
var user = await UserMgr.FindByEmailAsync(Model.Email);
user = new IdentityUser { UserName = email, Email = email }; // This must not be her !!!! We need to use user found by UserMgr.FindByEmailAsync(Model.Email);
string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
Complete simplified code is here:
private async Task GenerateResetToken()
{
var user = await UserMgr.FindByEmailAsync(Model.Email);
if (user == null)
{
Model.Error = "Not registered";
}
else
{
try
{
var _userManager = SignInMgr.UserManager;
UserMgr.FindByEmailAsync(Model.Email);
string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
if (resetToken == null)
{
log.Error("Cannot get token from GeneratePasswordResetTokenAsync");
}
else
{
// Reset token generated. Send email to user
}
}
catch (Exception exc)
{
log.Error(exc, $"Password reset failed due to the {exc.Message}");
}
}
}
My problem was that there was a typo in the email containing the ConfirmationToken:
<p>Please confirm your account by <a href=#ViewBag.CallbackUrl'>clicking here</a>.</p>
This meant the extra apostrophe was appended to the end of the ConfirmationToken.
D'oh!
My issue was that I was missing a <input asp-for="Input.Code" type="hidden" /> control in my Reset Password form
<form role="form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Input.Code" type="hidden" />

Categories

Resources