I am working on a .NET 7 project. I wanted to extend the email confirmation token expiration to five days. I used the following implementation:
public class CustomEmailConfirmationTokenProvider<TUser>
: DataProtectorTokenProvider<TUser> where TUser : class
{
public CustomEmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider,
IOptions<EmailConfirmationTokenProviderOptions> options,
ILogger<DataProtectorTokenProvider<TUser>> logger) : base(dataProtectionProvider,
options,
logger)
{
}
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{
}
}
To configure services:
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
options.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
})
.AddEntityFrameworkStores<TContext>()
.AddDefaultTokenProviders()
.AddTokenProvider<CustomEmailConfirmationTokenProvider<ApplicationUser>>("CustomEmailConfirmation");
services.Configure<IdentityOptions>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 3;
options.Lockout.AllowedForNewUsers = true;
});
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromMinutes(20));
services.Configure<EmailConfirmationTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(5));
Here are the service methods to generate and confirm the token respectively:
public virtual async Task<AuthenticationResult> GenerateEmailVerificationTokenAsync(string email)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return AuthenticationResult.Fail("No user exists with the specified email address.");
}
var token = _userManager.GenerateEmailConfirmationTokenAsync(user).Result;
if (string.IsNullOrWhiteSpace(token))
{
return AuthenticationResult.Fail("Email verification code could not be generated.");
}
return AuthenticationResult.EmailVerification(HttpUtility.UrlEncode(token));
}
public virtual async Task<AuthenticationResult> ConfirmEmailAsync(string userId, string code)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return AuthenticationResult.Fail("User is not found.");
}
var result = await _userManager.ConfirmEmailAsync(user!, code);
if (result.Succeeded)
{
return AuthenticationResult.Succeed("Email confirmed. Proceed to login.", user);
}
return AuthenticationResult.Fail(
result.Errors.FirstOrDefault() != null
? result.Errors.FirstOrDefault()!.Description
: "Failed to confirm the email.");
}
Here's how I have configured logging:
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore.Authentication": "Debug",
"Microsoft.AspNetCore.Authorization": "Debug"
}
}
The above code seems to work fine locally and on the dev environment (hosted on AWS ECS), but the same code gives an "InvalidToken" response on the Production environment (hosted on AWS ECS) on one or two requests per ten requests.
Token can be invalid for many reasons but how to find the exact reason when the response from ConfirmEmailAsync
is so conservative? The error description says "Invalid token" only. It seems almost impossible to debug this.
The solution is layered so services, DI resolver and API are all in separate layers. I have already tried Base64Encoding and Base64Decoding of the token, it didn't make any difference. I am not explicitly decoding the token parameter on the action method. Also, nothing is being logged related to the invalid token even when logging is set on the Debug level.
How can I find out why the EmailConfirmation
token becomes invalid intermittently?
Was able to fix this by persisting the Data protection key to the database.