I have a large netcore WebAPI application with multiple databases, each serving a different client. Data is shared via a queue that is handled by a netstandard 1.6 console app. Here's the relevant identity setup code in startup.cs (for the WebAPI app)
Services.AddEntityFramework() .AddEntityFrameworkNpgsql() .AddDbContext<Repository.IdentityDbContext>(); services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Cookies.ApplicationCookie.AutomaticChallenge = false; options.Cookies.ApplicationCookie.AutomaticAuthenticate = false; // Password settings options.Password.RequireDigit = Convert.ToBoolean(Configuration.GetSection("PasswordStrengthSettings")["RequireDigit"]); options.Password.RequiredLength = Convert.ToInt32(Configuration.GetSection("PasswordStrengthSettings")["MinimumPasswordLength"]); options.Password.RequireNonAlphanumeric = Convert.ToBoolean(Configuration.GetSection("PasswordStrengthSettings")["RequireNonAlphanumeric"]); options.Password.RequireUppercase = Convert.ToBoolean(Configuration.GetSection("PasswordStrengthSettings")["RequireUppercase"]); options.Password.RequireLowercase = Convert.ToBoolean(Configuration.GetSection("PasswordStrengthSettings")["RequireLowercase"]); options.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<ApplicationUser>))); // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(Convert.ToDouble(Configuration.GetSection("SecuritySettings")["LockoutDurationInMinutes"])); options.Lockout.MaxFailedAccessAttempts = Convert.ToInt32(Configuration.GetSection("SecuritySettings")["NumberOfAttemptsUntilLockout"]); // User settings options.User.RequireUniqueEmail = true; }).AddDefaultTokenProviders().AddEntityFrameworkStores<Repository.IdentityDbContext>(); services.Configure<DataProtectionTokenProviderOptions>(o => { o.Name = "Default"; o.TokenLifespan = TimeSpan.FromMinutes(Convert.ToDouble(Configuration.GetSection("SecuritySettings")["PasswordChangeTokenExpirationInMinutes"])); });
Here's the (identical) code in my DI setup in my console app:
services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Cookies.ApplicationCookie.AutomaticChallenge = false; options.Cookies.ApplicationCookie.AutomaticAuthenticate = false; // Password settings options.Password.RequireDigit = Convert.ToBoolean(configuration.GetSection("PasswordStrengthSettings")["RequireDigit"]); options.Password.RequiredLength = Convert.ToInt32(configuration.GetSection("PasswordStrengthSettings")["MinimumPasswordLength"]); options.Password.RequireNonAlphanumeric = Convert.ToBoolean(configuration.GetSection("PasswordStrengthSettings")["RequireNonAlphanumeric"]); options.Password.RequireUppercase = Convert.ToBoolean(configuration.GetSection("PasswordStrengthSettings")["RequireUppercase"]); options.Password.RequireLowercase = Convert.ToBoolean(configuration.GetSection("PasswordStrengthSettings")["RequireLowercase"]); options.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<ApplicationUser>))); // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(Convert.ToDouble(configuration.GetSection("SecuritySettings")["LockoutDurationInMinutes"])); options.Lockout.MaxFailedAccessAttempts = Convert.ToInt32(configuration.GetSection("SecuritySettings")["NumberOfAttemptsUntilLockout"]); // User settings options.User.RequireUniqueEmail = true; }).AddDefaultTokenProviders().AddEntityFrameworkStores<Repository.IdentityDbContext>(); services.Configure<DataProtectionTokenProviderOptions>(o => { o.Name = "Default"; o.TokenLifespan = TimeSpan.FromMinutes(Convert.ToDouble(configuration.GetSection("SecuritySettings")["PasswordChangeTokenExpirationInMinutes"])); });
When the console app generates a password change token, it is rejected as "invalid" by Identity in the API endpoint to change password. If I create a password change token for the same user via the WebAPI, it works fine for the same endpoint. Thoughts? Any help would be most appreciated, as I'm sure I'm missing something!
Edit: I have ensured that the PasswordChangeTokenExpiration setting is identical in both json config files and is being picked up by both apps. Also, here's the code that generates a token and sends an email: both apps call the same function (it's in its own DLL):
private async Task SendNewUserEmail() { if (!_serviceResult.HasAnyError) { var token = await _userManager.GeneratePasswordResetTokenAsync(_userModel); if (token != null) { var template = _emailTemplates.GetEmailTemplate("new-user"); var emailBody = template.Body .Replace("~~~BaseUrl~~~", _emailSettings.FrontEndBaseUrl.Replace("~~~Subdomain~~~", _currentUserTools.GetCurrentDestination())) .Replace("~~~Token~~~", HtmlEncoder.Default.Encode(token)) .Replace("~~~FirstName~~~", HtmlEncoder.Default.Encode(_userModel.FirstName ?? "")) .Replace("~~~LastName~~~", HtmlEncoder.Default.Encode(_userModel.LastName ?? "")) .Replace("~~~Type~~~", HtmlEncoder.Default.Encode("NEW_USER")) .Replace("~~~Email~~~", HtmlEncoder.Default.Encode(_userModel.Email)); await _emailSender.SendEmail( new EmailTools.Models.MailParticipant(_emailSettings.DefaultFromEmailName, _emailSettings.DefaultFromEmailAddress), new EmailTools.Models.MailParticipant("", _userModel.Email), template.Subject, emailBody); } _serviceResult.Data = UserDto.GetDto(_userModel); } }