How to Store Tokens in ASP.NET Core Identity

How To Store Tokens in ASP.NET Core Identity Database Table

In this article, I will discuss How to Store Tokens in the ASP.NET Core Identity Database Table. Please read our previous article discussing How to Implement the Forgot Password in ASP.NET Core Identity.

Why are the Email Confirmation and Forgot Password Tokens not stored in the AspNetUserTokens Table?

In ASP.NET Core Identity, the AspNetUserTokens table is designed to store tokens for various purposes such as authentication, two-factor authentication (2FA), etc. However, email confirmation and forgot password tokens are not stored in the AspNetUserTokens table by default. Here are the primary reasons for this design choice:

Security Considerations:

Storing sensitive tokens like Email Confirmations and Forgot Passwords in the database could expose security risks. If the database is compromised, attackers could gain access to these tokens, potentially allowing unauthorized changes to email addresses or passwords. That is, the attacker confirms email addresses or resets passwords without the user’s knowledge or consent. By not persisting sensitive tokens, ASP.NET Core Identity reduces the risk of token theft or misuse.

One-Time Use and Short Lifespan:

Both email confirmation and forgot password tokens are designed for one-time use and typically have a short lifespan (usually minutes to an hour). They are meant to be used quickly after generation, so storing them in a database is often unnecessary and inefficient as they become invalid after a single use or upon expiration.

Stateless Approach:

ASP.NET Core Identity uses a stateless approach for these tokens. Instead of storing the token, it generates the token dynamically using user-specific data (e.g., user ID, email) combined with a secret key. When the token is submitted for verification, the system regenerates it using the same parameters and secret key and validates it against the provided token. Hence, a database lookup is not required. This eliminates the need to store the token in a database, reducing complexity and database dependency.

How Do We Store and Delete Forgot Password Tokens in ASP.NET Core Identity AspNetUserTokens table?

By default, ASP.NET Core Identity does not store email confirmation and forgot password tokens in the AspNetUserTokens table. However, if you want, you can manually store these tokens in the AspNetUserTokens table. This can be useful for auditing or debugging (but be cautious with the security implications) purposes.

Storing the Forgot Password Token in the AspNetUserTokens table:

Storing and deleting a forgot password token in the AspNetUserTokens table requires a few steps. Let us proceed and see how we can explicitly store and delete the Forgot Password Token in the AspNetUserTokens table.

Generate the Password Reset Token

Use the GeneratePasswordResetTokenAsync method provided by the UserManager<TUser> service to create a password reset token.

var token = await userManager.GeneratePasswordResetTokenAsync(user);

Store the Token in AspNetUserTokens

Then, we need to use the SetAuthenticationTokenAsync method to explicitly store the generated token in the AspNetUserTokens table.

await userManager.SetAuthenticationTokenAsync(
    user,
    "ResetPassword",       // loginProvider
    "ResetPasswordToken",  // tokenName
    token                  // tokenValue
);

Here:

  • user: The user for whom the token is being generated.
  • “ResetPassword”: A custom loginProvider identifier.
  • “ResetPasswordToken”: A custom tokenName identifier.
  • token: The generated password reset token.
Modify the SendForgotPasswordEmail Method:

In our example, the SendForgotPasswordEmail private method is used to generate the Password Reset token. So, modify the SendForgotPasswordEmail method of the Account Controller as follows:

private async Task SendForgotPasswordEmail(string? email, ApplicationUser? user)
{
    // Generate the reset password token
    var token = await userManager.GeneratePasswordResetTokenAsync(user);

    // Save the token into the AspNetUserTokens database table
    await userManager.SetAuthenticationTokenAsync(user, "ResetPassword", "ResetPasswordToken", token);

    // Build the password reset link
    var passwordResetLink = Url.Action("ResetPassword", "Account",
        new { Email = email, Token = token }, protocol: HttpContext.Request.Scheme);

    // Encode the link to prevent XSS attacks
    var safeLink = HtmlEncoder.Default.Encode(passwordResetLink);

    // Create the email subject
    var subject = "Reset Your Password";

    // Craft HTML message body
    var messageBody = $@"
    <div style=""font-family: Arial, Helvetica, sans-serif; font-size: 16px; color: #333; line-height: 1.5; padding: 20px;"">
        <h2 style=""color: #007bff; text-align: center;"">Password Reset Request</h2>
        <p style=""margin-bottom: 20px;"">Hi {user.FirstName} {user.LastName},</p>
        
        <p>We received a request to reset your password for your <strong>Dot Net Tutorials</strong> account. If you made this request, please click the button below to reset your password:</p>
        
        <div style=""text-align: center; margin: 20px 0;"">
            <a href=""{safeLink}"" 
               style=""background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none; font-weight: bold; border-radius: 5px; display: inline-block;"">
                Reset Password
            </a>
        </div>
        
        <p>If the button above doesn’t work, copy and paste the following URL into your browser:</p>
        <p style=""background-color: #f8f9fa; padding: 10px; border: 1px solid #ddd; border-radius: 5px;"">
            <a href=""{safeLink}"" style=""color: #007bff; text-decoration: none;"">{safeLink}</a>
        </p>
        
        <p>If you did not request to reset your password, please ignore this email or contact support if you have concerns.</p>
        
        <p style=""margin-top: 30px;"">Thank you,<br />The Dot Net Tutorials Team</p>
    </div>";

    // Send the email
    await emailSender.SendEmailAsync(email, subject, messageBody, IsBodyHtml: true);
}
Deleting Password Reset Token from AspNetUserTokens Table

Once the token is used (i.e., the user has successfully reset their password) or expires, it should be removed from the AspNetUserTokens table to maintain security and prevent reuse. To delete the token, we need to use the following RemoveAuthenticationTokenAsync method. Please ensure that the loginProvider and tokenName match the values used when storing the token.

await userManager.RemoveAuthenticationTokenAsync(
    user,
    "ResetPassword",
    "ResetPasswordToken"
);
Modified ResetPassword Method

In our example, the ResetPassword Post action method is used to reset the user password. So, once the password is reset successfully, we need to delete the token from the AspNetUserTokens database table. So, please modify the ResetPassword Post action method of the Account Controller as follows:

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
    // Check if the incoming model passes all validation rules specified in the data annotations.
    if (ModelState.IsValid)
    {
        // Find the user in the database using the provided email address.
        var user = await userManager.FindByEmailAsync(model.Email);

        // Proceed only if the user exists in the database.
        if (user != null)
        {
            // Attempt to reset the user's password using the token and the new password provided in the model.
            var result = await userManager.ResetPasswordAsync(user, model.Token, model.Password);

            //Once the Password is Reset, remove the token from the database
            await userManager.RemoveAuthenticationTokenAsync(user, "ResetPassword", "ResetPasswordToken");

            // If the password reset operation is successful, redirect the user to the Reset Password Confirmation page.
            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "Account");
            }

            // If the password reset fails, loop through the list of errors returned by Identity
            // and add them to the ModelState to display on the view.
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error.Description); // Add each error description to ModelState.
            }

            // Return the model back to the view so the user can fix any validation errors.
            return View(model);
        }

        // If the user is not found, redirect to the Reset Password Confirmation page to avoid
        // revealing whether an account exists for the provided email.
        // This approach prevents account enumeration attacks.
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }

    // If the model state is invalid (e.g., missing or incorrect data), return the same view
    // and display validation errors to the user.
    return View(model);
}
Testing the Functionalities:

With the above changes in place, run the application, go to the Login page, and click the Forgot Password link, as shown in the image below.

How to Implement the Forgot Password Functionality in ASP.NET Core Identity

From the next screen, enter your Email ID and click the submit button, as shown in the image below.

How to Implement the Forgot Password Functionality in ASP.NET Core Identity

Once you enter the Email address and click on the Submit button, an email will be sent to the above Email ID if the given Email ID exists in the database. At the same time, it should have made an entry into the AspNetUserTokens table, as shown in the below image.

Forgot Password in ASP.NET Core Identity

The user will receive the Password Reset Email, something like the one below, in his email address.

How to Reset User Password in ASP.NET Core Identity using Forget Password

The following Password Reset page will open once the user clicks the above button or link. The user must enter the Password, Confirm Password, and click the Reset Password button, as shown in the image below.

How to Reset User Password in ASP.NET Core Identity using Forget Password

Once the user provides the Password and Confirm Password and clicks the Reset Password button, the password will reset, and you will get the following message.

Forgot Password in ASP.NET Core Identity

Once the password is reset successfully, if you verify the AspNetUserTokens table, then you will see the record should be deleted, as shown in the image below.

Forgot Password in ASP.NET Core Identity

Storing Email Confirmation Token in AspNetUserTokens table:

First, modify the SendConfirmationEmail method, which generates the Email Confirmation token. Once the token is generated, we are also storing that token in the database.

private async Task SendConfirmationEmail(string email, ApplicationUser user)
{
    // Generate the email confirmation token
    var token = await userManager.GenerateEmailConfirmationTokenAsync(user);

    // Save the token into the AspNetUserTokens database table
    await userManager.SetAuthenticationTokenAsync(user, "EmailConfirmation", "EmailConfirmationToken", token);

    // Build the confirmation callback URL
    // protocol: HttpContext.Request.Scheme: Generate the fully qualified URL with domain
    var confirmationLink = Url.Action("ConfirmEmail", "Account",
        new { UserId = user.Id, Token = token }, protocol: HttpContext.Request.Scheme);

    // Encode the link to prevent XSS and other injection attacks
    var safeLink = HtmlEncoder.Default.Encode(confirmationLink);

    // Craft a more polished email subject
    var subject = "Welcome to Dot Net Tutorials! Please Confirm Your Email";

    // Create a professional HTML body
    // Customize inline styles, text, and branding as needed
    var messageBody = $@"
        <div style=""font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#333;"">
            <p>Hi {user.FirstName} {user.LastName},</p>

            <p>Thank you for creating an account at <strong>Dot Net Tutorials</strong>.
            To start enjoying all of our features, please confirm your email address by clicking the button below:</p>

            <p>
                <a href=""{safeLink}"" 
                   style=""background-color:#007bff;color:#fff;padding:10px 20px;text-decoration:none;
                          font-weight:bold;border-radius:5px;display:inline-block;"">
                    Confirm Email
                </a>
            </p>

            <p>If the button doesn’t work for you, copy and paste the following URL into your browser:
                <br />
                <a href=""{safeLink}"" style=""color:#007bff;text-decoration:none;"">{safeLink}</a>
            </p>

            <p>If you did not sign up for this account, please ignore this email.</p>

            <p>Thanks,<br />
            The Dot Net Tutorials Team</p>
        </div>
    ";

    //Send the Confirmation Email to the User Email Id
    await emailSender.SendEmailAsync(email, subject, messageBody, true);
}
Modify ConfirmEmail Method:

Next, modify the ConfirmEmail method as follows. Once the email is confirmed, we will remove the token from the database.

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string UserId, string Token)
{
    if (string.IsNullOrEmpty(UserId) || string.IsNullOrEmpty(Token))
    {
        // Provide a descriptive error message for the view
        ViewBag.ErrorMessage = "The link is invalid or has expired. Please request a new one if needed.";
        return View();
    }

    //Find the User by Id
    var user = await userManager.FindByIdAsync(UserId);
    if (user == null)
    {
        // Provide a descriptive error for a missing user scenario
        ViewBag.ErrorMessage = "We could not find a user associated with the given link.";
        return View();
    }

    // Attempt to confirm the email
    var result = await userManager.ConfirmEmailAsync(user, Token);

    //Once the Email is Confirm, remove the token from the database
    await userManager.RemoveAuthenticationTokenAsync(user, "EmailConfirmation", "EmailConfirmationToken");

    if (result.Succeeded)
    {
        ViewBag.Message = "Thank you for confirming your email address. Your account is now verified!";
        return View();
    }

    // If confirmation fails
    ViewBag.ErrorMessage = "We were unable to confirm your email address. Please try again or request a new link.";
    return View();
}

Note: Storing sensitive tokens in the database can introduce security vulnerabilities. Only implement this approach for testing or specific scenarios where it’s absolutely necessary. Always prioritize security best practices to protect user data.

What Is the Default Token Lifetime?

ASP.NET Core Identity has a default token lifetime set in its configuration. Typically, this lifetime is set to 24 hours for tokens like email confirmation and password reset tokens unless explicitly configured.

How Do We Set Token Lifetime explicitly in ASP.NET Core Identity?

By default, ASP.NET Core Identity uses the DataProtectorTokenProvider<TUser> for generating tokens, and the DataProtectionTokenProviderOptions specify its lifetime. In ASP.NET Core, we need to configure the Token lifetime in the Program class. So, please add the following code to the Program class:

// Configure token lifespan
builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
{
    // Set token lifespan to 2 hours
    options.TokenLifespan = TimeSpan.FromHours(2);
});

Task? How do we set different life spans for different types of Tokens? Please do this by yourself and once done, please put the code in the comment section.

In the next article, I will discuss Change Password in ASP.NET Core Identity. In this article, I explain How to Store Tokens in ASP.NET Core Identity. I hope you enjoy this article, How to Store Tokens in ASP.NET Core Identity.

Registration Open For New Online Training

Enhance Your Professional Journey with Our Upcoming Live Session. For complete information on Registration, Course Details, Syllabus, and to get the Zoom Credentials to attend the free live Demo Sessions, please click on the below links.

Leave a Reply

Your email address will not be published. Required fields are marked *