Implementing User Confirmation and Password Reset with One ASP.NET Identity – Pain or Pleasure

Recently, I was trying to introduce GitHub and Twitter login for my TextUml, it was using the simple membership which has out of the box support for Twitter login, my attempt to create a client for GitHub OAuth2 failed, it seems the OAuthWebSecurity was receiving different query string name for provider from GitHub, anyway I did not want to invest too much time as Microsoft is coming up with a new Membership system called ASP.NET Identity (also the default in ASP.NET MVC 5) and I want to give it a try as the server side of TextUml is already running on all the edge versions of different packages.

Once installed, I quickly realized that it is not at all compatible with earlier versions of Membership, you have to create your own solution to migrate your existing data but the initial thing that surprised me that there is no built-in support for user confirmation and password reset. The user confirmation is when a user signs up, your application sends an email with a link, when the user visits the link the account is confirmed. And reset password is when user forgets password, s/he provides the email and the application sends an email (if matched) with a link, when the link is visited, it allows the user to set new password. Pretty standard stuff and these are available in the current simple membership and it worked pretty nicely in my projects. To implement these features it is obvious that I have to store few additional data like confirmation token, a flag whether it is confirmed, reset password token and its expiration timestamp. Though the above MSDN blog post has a bullet list of features but I find that the customization is more towards the additional attributes of user rather than injecting your own logic in the login process, you will find it shortly. So to store this data, I have created a new class called Token:

Token

              [Table("Tokens"), CLSCompliant(false)]
              public class Token
              {
                  private static readonly TimeSpan ResetPasswordExpireTimeframe =
                      TimeSpan.FromDays(1);
              
                  [Obsolete("Used by the underlying ORM.")]
                  public Token()
                  {
                  }
              
                  public Token(string userId, bool requiresActivation)
                  {
                      UserId = userId;
              
                      if (requiresActivation)
                      {
                          ActivationToken = GenerateToken();
                      }
                      else
                      {
                          MarkActivated();
                      }
                  }
              
                  [Key]
                  public string UserId { get; set; }
              
                  [ForeignKey("UserId")]
                  public virtual User User { get; set; }
              
                  public DateTime? ActivatedAt { get; set; }
              
                  [StringLength(128)]
                  public string ActivationToken { get; set; }
              
                  [StringLength(128)]
                  public string ResetPasswordToken { get; set; }
              
                  public DateTime? ResetPasswordTokenExpiredAt { get; set; }
              
                  public bool HasResetPasswordTokenExpired
                  {
                      get
                      {
                          return ResetPasswordTokenExpiredAt > Clock.UtcNow();
                      }
                  }
              
                  public bool CanActivate(string token)
                  {
                      return ActivatedAt == null &&
                          string.Equals(
                              ActivationToken,
                              token,
                              StringComparison.OrdinalIgnoreCase);
                  }
              
                  public void MarkActivated()
                  {
                      if (ActivatedAt != null)
                      {
                          return;
                      }
              
                      ActivatedAt = Clock.UtcNow();
                  }
              
                  public void GenerateResetPasswordToken()
                  {
                      ResetPasswordToken = GenerateToken();
                      ResetPasswordTokenExpiredAt = Clock.UtcNow()
                          .Add(ResetPasswordExpireTimeframe);
                  }
              
                  public void ExpireResetPasswordToken()
                  {
                      ResetPasswordTokenExpiredAt = Clock.UtcNow();
                  }
              
                  private static string GenerateToken()
                  {
                      var buffer = new byte[16];
              
                      using (var crypto = new RNGCryptoServiceProvider())
                      {
                          crypto.GetBytes(buffer);
                      }
              
                      return HttpServerUtility.UrlTokenEncode(buffer);
                  }
              }
              

The Token has one-to-one relation with the User, I am using ActivatedAt as a confirmation flag, but it can be a boolean property as well, there are few other helper methods which we would see in action shortly. Now, whenever a user is going to be created I have to create the corresponding token as well. For creating a new user it has CreateLocalUser method of the IdentityStoreManager class. We can use it for creating the user then we can persist the corresponding token, but in that case we are not treating it as single unit of work (multiple SaveChanges of DbContext). So it is better if I create an overloaded version, but in that case I have to create an inherited class of IdentityStoreManager.

IdentityStoreManager

              [CLSCompliant(false)]
              public class AppIdentityStoreManager : IdentityStoreManager
              {
                  private readonly AppIdentityStoreContext context;
              
                  public AppIdentityStoreManager(AppIdentityStoreContext storeContext) :
                      base(storeContext)
                  {
                      context = storeContext;
                  }
              
                  public async Task<string> CreateLocalUser(
                      IUser user,
                      string password,
                      bool requiresActivation)
                  {
                      ValidateUser(user);
                      ValidatePassword(password);
              
                      if (!(await context.Users.Create(user)))
                      {
                          return null;
                      }
              
                      if (!(await context.Secrets.Create(
                          new UserSecret(user.UserName, password))))
                      {
                          return null;
                      }
              
                      if (!(await context.Logins.Add(
                          new UserLogin(user.Id, LocalLoginProvider, user.UserName))))
                      {
                          return null;
                      }
              
                      var token = new Token(user.Id, requiresActivation);
              
                      var dataContext = (DataContext)context.DbContext;
                      dataContext.Tokens.Add(token);
              
                      await dataContext.SaveChangesAsync();
              
                      return token.ActivationToken;
                  }
              
                  private void ValidatePassword(string password)
                  {
                      if (string.IsNullOrWhiteSpace(password))
                      {
                          throw new ArgumentException("Password is required.", "password");
                      }
              
                      string error;
              
                      if (!PasswordValidator.Validate(password, out error))
                      {
                          throw new IdentityException(error);
                      }
                  }
              
                  private void ValidateUser(IUser user)
                  {
                      if (user == null)
                      {
                          throw new ArgumentNullException("user");
                      }
              
                      if (string.IsNullOrWhiteSpace(user.UserName))
                      {
                          throw new ArgumentException("user.UserName");
                      }
              
                      string error;
              
                      if (!UserNameValidator.Validate(user.UserName, out error))
                      {
                          throw new IdentityException(error);
                      }
                  }
              }
              

If you compare the above code with the original CreateLocalUser method, you would see I had to duplicate almost all of the code instead of reusing the existing building blocks.

Now, that we have created the user, lets move to the login part, as you can guess we also have to consider the confirmation flag along with the user name and password, unless the user is confirmed s/he is not allowed to login even the credential matches. The user name and password matching happens in the Validate method of UserSecretStore class and the confirmation checking should go there. The Validate method is not virtual, but the good news is the UserSecretStore class implements IUserSecretStore interface, which means we can create a new class with our custom logic that implements the interface and plugs it in.

UserSecretStore

              [CLSCompliant(false)]
              public class AppUserSecretStore : IUserSecretStore
              {
                  private readonly DataContext dataContext;
                  private readonly UserSecretStore<UserSecret> store;
              
                  public AppUserSecretStore(DataContext db)
                  {
                      dataContext = db;
                      store = new UserSecretStore<UserSecret>(db);
                  }
              
                  public Task<bool> Delete(string userName)
                  {
                      return store.Delete(userName);
                  }
              
                  public Task<bool> Create(IUserSecret userSecret)
                  {
                      return store.Create(userSecret);
                  }
              
                  public Task<bool> Update(string userName, string newSecret)
                  {
                      return store.Update(userName, newSecret);
                  }
              
                  public async Task<bool> Validate(string userName, string loginSecret)
                  {
                      var activated = await dataContext.Tokens
                          .AnyAsync(t =>
                              t.ActivatedAt != null &&
                              t.User.UserName == userName);
              
                      return activated &&
                          await store.Validate(userName, loginSecret);
                  }
              
                  public Task<IUserSecret> Find(string userName)
                  {
                      return store.Find(userName);
                  }
              }
              

In the above, we created an instance of the same UserSecretStore class internally and delegated all our method calls to it, the only exception is the Validate method where we are checking confirmation status before delegating. Now, to plug it in we have to create another inherited class from IdentityStoreContext:

IdentityStoreContext

              [CLSCompliant(false)]
              public class AppIdentityStoreContext : IdentityStoreContext
              {
                  public AppIdentityStoreContext(DataContext dataContext) :
                      base(dataContext)
                  {
                      Secrets = new AppUserSecretStore(dataContext);
                  }
              }
              

Next, In order to keep my rest of the application code mixing with this new membership code I created a MembershipService that as a façade. With the above sign-up and sign-in it looks like the following:

MembershipService

              public class MembershipService : IMembershipService
              {
                  private readonly DataContext dataContext;
              
                  public MembershipService(
                      DataContext dataContext,
                      Func<HttpContextBase> lazyHttpContext)
                  {
                      this.dataContext = dataContext;
                      LazyHttpContext = lazyHttpContext;
              
                      var identityContext = new AppIdentityStoreContext(this.dataContext);
                      IdentityManager = new AppIdentityStoreManager(identityContext);
                      AuthenticationManager = new IdentityAuthenticationManager(
                          IdentityManager);
                  }
              
                  public IdentityAuthenticationManager AuthenticationManager
                  {
                      get;
                      protected set;
                  }
              
                  protected AppIdentityStoreManager IdentityManager { get; set; }
              
                  protected Func<HttpContextBase> LazyHttpContext { get; private set; }
              
                  public async Task<string> Signup(
                      string email,
                      string password,
                      bool requiresActivation)
                  {
                      var user = new User(email);
              
                      var token = await IdentityManager.CreateLocalUser(
                          user,
                          password,
                          requiresActivation);
              
                      return token;
                  }
              
                  public async Task<bool> SignIn(
                      string email,
                      string password,
                      bool persist)
                  {
                      return await AuthenticationManager.CheckPasswordAndSignIn(
                          LazyHttpContext(),
                          email,
                          password,
                          persist);
                  }
              }
              

The next three features are rather simple and easy as we have all the necessary things in place:


              public async Task<bool> Activate(string email, string token)
              {
                  var userId = await IdentityManager.GetUserIdForLocalLogin(email);
              
                  if (userId == null)
                  {
                      return false;
                  }
              
                  var activation = await dataContext.Tokens.FindAsync(userId);
              
                  if ((activation == null) || !activation.CanActivate(token))
                  {
                      return false;
                  }
              
                  activation.MarkActivated();
              
                  await dataContext.SaveChangesAsync();
              
                  return true;
              }
              
              public async Task<string> ForgotPassword(string email)
              {
                  var userId = await IdentityManager.GetUserIdForLocalLogin(email);
              
                  if (userId == null)
                  {
                      return null;
                  }
              
                  var token = await dataContext.Tokens.FindAsync(userId);
              
                  token.GenerateResetPasswordToken();
              
                  await dataContext.SaveChangesAsync();
              
                  return token.ResetPasswordToken;
              }
              
              public async Task<bool> ResetPassword(string token, string password)
              {
                  var reset = await dataContext.Tokens
                      .FirstOrDefaultAsync(t => t.ResetPasswordToken == token);
              
                  if (reset == null || reset.HasResetPasswordTokenExpired)
                  {
                      return false;
                  }
              
                  var user = await IdentityManager.Context.Users.Find(reset.UserId);
              
                  if (user == null)
                  {
                      return false;
                  }
              
                  var hasChanged = await IdentityManager.ChangePassword(
                      user.UserName,
                      password,
                      password);
              
                  if (hasChanged)
                  {
                      reset.ExpireResetPasswordToken();
                      await dataContext.SaveChangesAsync();
                  }
              
                  return hasChanged;
              }
              

The next part is integrating the external login support which does not require any modification, for GitHub OAuth2 I have also created a Owin authentication module like the other built-in modules, you can check the GitHub implementation over here and the nuget package.

Now, everything seems to run smoothly until I found out that UserName of User needs to be unique and seems silly as I myself have same user name in Facebook and GitHub. It turns out that uniqueness checking is in the ValidateEntity method of IdentityDbContext. All I have to overcome this issue is override this method:


              protected override DbEntityValidationResult ValidateEntity(
                  DbEntityEntry entityEntry,
                  IDictionary<object, object> items)
              {
                  return new DbEntityValidationResult(entityEntry, new DbValidationError[0]);
              }
              

But it introduces another issue, now we are allowing to have same user name for internal users and to restrict it we need a little modification in our CreateLocalUser method:

CreateLocalUser

              public async Task<string> CreateLocalUser(
                  IUser user,
                  string password,
                  bool requiresActivation)
              {
                  ValidateUser(user);
                  ValidatePassword(password);
              
                  if (await GetUserIdForLocalLogin(user.UserName) != null)
                  {
                      throw new IdentityException("User name already exists.");
                  }
              
                  if (!(await context.Users.Create(user)))
                  {
                      return null;
                  }
              
                  if (!(await context.Secrets.Create(
                      new UserSecret(user.UserName, password))))
                  {
                      return null;
                  }
              
                  if (!(await context.Logins.Add(
                      new UserLogin(user.Id, LocalLoginProvider, user.UserName))))
                  {
                      return null;
                  }
              
                  var token = new Token(user.Id, requiresActivation);
              
                  var dataContext = (DataContext)context.DbContext;
                  dataContext.Tokens.Add(token);
              
                  await dataContext.SaveChangesAsync();
              
                  return token.ActivationToken;
              }
              

And we are done, you will find the complete implementation in TextUml.

Few more observations:

  • Although there is an interface IUser but creating a custom User and implementing this interface does not work external user as the method GetUserIdentityClaims of IdentityAuthenticationManager needs an concrete class of User not IUser .
  • Both UserRoles and UserSecrets in the database stores the name instead of the Id which is quite unusual should be avoided.
  • You cannot have the same name entity class (e.g. User, Role etc.) that is already here, though it seems more of an EF issue.
  • Overall I think it is doing whole lot of things that it should not do and If your application logic varies from what it provides out of the box you are going to end up implementing the same.

That's it for today.

Comments

blog comments powered by Disqus