Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where does AspnetIdentity stores UserTokens?

I have ASP.Net MVC5 application. While creating application when you select Individual Authentication visual studio use scaffolding and adds code for Login, Create User, Forgot Password etc. I am using this default implementation of ForgotPassword functionality. This is how the forgot password works.

1> User click on forgot password link.

2> User enters his email address.

3> Application creates new Token and sends email with return URL with Token in the query string.

4> User clicks on the link in email and gets redirected to ResetPassword page.

5> User enters new password and hit submit.

6> Application Validates the user and token and resets the password.

Question.

1> After receiving email User may not click the link immediately, he may click after sometime or even after few days. In between where does ASP.NET application stores the token? I don’t see it in the database.

2> Is there any expiration on the token.

3> On forgot password screen user enters his email and click submit to receive email with token. Let’s say he does this 3 times. So he receives 3 emails with 3 different tokens. Then he clicks on the link from any one email and resets the password. What happens to unused tokens in other 2 emails, are they still valid? Can user click on link in other email and be able to reset password?

like image 688
LP13 Avatar asked Jul 30 '16 05:07

LP13


1 Answers

Basically if you look at the user created by ASP.Net Identity you would see a column SecurityStamp which is basically hash and is used in all password related scenarios. In fact it is changed when a user changes his password.

When you click on reset password SecurityStamp associated with that user is used to generate the token that is sent in email. See the source code in GenerateAsync method here (there are 2 providers)

https://github.com/aspnet/Identity/blob/dev/src/Microsoft.AspNetCore.Identity/TotpSecurityStampBasedTokenProvider.cs

public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
    {
        if (manager == null)
        {
            throw new ArgumentNullException(nameof(manager));
        }
        var token = await manager.CreateSecurityTokenAsync(user);
        var modifier = await GetUserModifierAsync(purpose, manager, user);
        return Rfc6238AuthenticationService.GenerateCode(token, modifier).ToString("D6", CultureInfo.InvariantCulture);
}

or

https://github.com/aspnet/Identity/blob/dev/src/Microsoft.AspNetCore.Identity/DataProtectionTokenProvider.cs

    public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
    {
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        var ms = new MemoryStream();
        var userId = await manager.GetUserIdAsync(user);
        using (var writer = ms.CreateWriter())
        {
            writer.Write(DateTimeOffset.UtcNow);
            writer.Write(userId);
            writer.Write(purpose ?? "");
            string stamp = null;
            if (manager.SupportsUserSecurityStamp)
            {
                stamp = await manager.GetSecurityStampAsync(user);
            }
            writer.Write(stamp ?? "");
        }
        var protectedBytes = Protector.Protect(ms.ToArray());
        return Convert.ToBase64String(protectedBytes);
    }

Once a user clicks on the link and submits new password token sent in email also gets submitted and is validated against the SecurityStamp.

    public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
    {
        if (manager == null)
        {
            throw new ArgumentNullException(nameof(manager));
        }
        int code;
        if (!int.TryParse(token, out code))
        {
            return false;
        }
        var securityToken = await manager.CreateSecurityTokenAsync(user);
        var modifier = await GetUserModifierAsync(purpose, manager, user);
        return securityToken != null && Rfc6238AuthenticationService.ValidateCode(securityToken, code, modifier);
}

Or

    public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
    {
        try
        {
            var unprotectedData = Protector.Unprotect(Convert.FromBase64String(token));
            var ms = new MemoryStream(unprotectedData);
            using (var reader = ms.CreateReader())
            {
                var creationTime = reader.ReadDateTimeOffset();
                var expirationTime = creationTime + Options.TokenLifespan;
                if (expirationTime < DateTimeOffset.UtcNow)
                {
                    return false;
                }

                var userId = reader.ReadString();
                var actualUserId = await manager.GetUserIdAsync(user);
                if (userId != actualUserId)
                {
                    return false;
                }
                var purp = reader.ReadString();
                if (!string.Equals(purp, purpose))
                {
                    return false;
                }
                var stamp = reader.ReadString();
                if (reader.PeekChar() != -1)
                {
                    return false;
                }

                if (manager.SupportsUserSecurityStamp)
                {
                    return stamp == await manager.GetSecurityStampAsync(user);
                }
                return stamp == "";
            }
        }
        // ReSharper disable once EmptyGeneralCatchClause
        catch
        {
            // Do not leak exception
        }
        return false;
}

Once it successfully validates the token Identity system further updates the user details with new SecurityStamp. With all that said answer to your questions

Ans 1 - Token is not stored and link in email is active immediately. I have a fully tested production system and neither me nor users have to wait for the link to be active.

Ans 2- I think the default is 1 day. You can change it by adding below code in Create method of ApplicationUserManager Class

if (dataProtectionProvider != null)
 {
  manager.UserTokenProvider =
   new DataProtectorTokenProvider<ApplicationUser>
      (dataProtectionProvider.Create("ASP.NET Identity"))
      {                    
         TokenLifespan = TimeSpan.FromHours(1) //Any custom TimeSpan
      };
 }

Ans 3 - Only 1 will work i.e. whichever user clicks first because after that security stamp changes and it will invalidate the token in other 2 emails.

Hope this helps.

like image 185
Ravi A. Avatar answered Sep 22 '22 02:09

Ravi A.