I have (2) Identity systems
, each in its own Context
:
1: CustomerContext : IdentityDbContext<CustomerUser>
2: ApplicationContext : IdentityDbContext<ApplicationUser>
I have successfully registered them in ASP.NET Core 3.0 API Startup file. One using 'AddIdentity' and the other 'AddIdentityCore'
I also added 'AddDefaultTokenProviders' to both of them. Though it builds and runs, the problem occurs when I attempt to use token providers, such as GenerateEmailConfirmationTokenAsync or GeneratePasswordResetTokenAsync.
If I remove one of the 'AddDefaultTokenProviders' from the registration, then using tokens works for the Identity with 'AddDefaultTokenProviders', its when both include AddDefaultTokenProviders, neither work. I get these exceptions (I have trimmed them up a bit for brevity):
System.NotSupportedException: No IUserTwoFactorTokenProvider named 'Default' is registered.
- at Microsoft.AspNetCore.Identity.UserManager.GenerateUserTokenAsync(GenerateEmailConfirmationTokenAsync)
OR
- at Microsoft.AspNetCore.Identity.UserManager.GenerateUserTokenAsync(GeneratePasswordResetTokenAsync)
These are the Identity registrations in Startup.cs:
CustomerUser
services.AddIdentity<CustomerUser, CustomerRole>(options =>
{
options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<CustomerContext>()
.AddDefaultTokenProviders(); // <-- CANNOT HAVE (2)
ApplicationUser
var builder = services.AddIdentityCore<ApplicationUser>(options =>
{
options.Password.RequiredLength = 6;
});
builder = new IdentityBuilder(builder.UserType, typeof(ApplicationRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationContext>();
builder.AddDefaultTokenProviders(); // <-- CANNOT HAVE (2)
I came across an article mentioning that IdentityOptions is singleton and cannot call AddDefaultTokenProviders twice. But no resolution how to fix it.
How do I include Default Token Providers for both Identities? Do I need to create custom Token Providers? If so, how? I do not need any token customization, I just need the default token behavior.
Thank you.
I resolved this by adding the second Identity Service as follows:
services.AddIdentityCore<CustomerUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<TenantDataContext>()
.AddTokenProvider<DataProtectorTokenProvider<CustomerUser>>(TokenOptions.DefaultProvider);
The difference is to call .AddTokenProvider
as indicated instead of .AddDefaultTokenProviders()
You should add Token Provider.
public class PasswordResetTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
{
public PasswordResetTokenProvider(IDataProtectionProvider dataProtectionProvider,
IOptions<PasswordResetTokenProviderOptions> options,
ILogger<DataProtectorTokenProvider<TUser>> logger)
: base(dataProtectionProvider, options, logger)
{
}
}
public class PasswordResetTokenProviderOptions : DataProtectionTokenProviderOptions
{
public PasswordResetTokenProviderOptions()
{
Name = "PasswordResetTokenProvider";
TokenLifespan = TimeSpan.FromDays(3);
}
}
Startup.cs
services.AddIdentity<AppTenantUser, AppTenantRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.Tokens.EmailConfirmationTokenProvider = "emailConfirmation";
config.Tokens.PasswordResetTokenProvider = "passwordReset";
config.Password.RequiredLength = 0;
config.Password.RequiredUniqueChars = 0;
config.Password.RequireLowercase = false;
config.Password.RequireUppercase = false;
config.Password.RequireDigit = false;
config.Password.RequireNonAlphanumeric = false;
config.User.RequireUniqueEmail = true;
config.User.AllowedUserNameCharacters = "abcçdefghiıjklmnoöpqrsştuüvwxyzABCÇDEFGHIİJKLMNOÖPQRSŞTUÜVWXYZ0123456789-._@+'#!/^%{}*";
})
.AddEntityFrameworkStores<TenantDbContext>()
.AddDefaultTokenProviders()
.AddTokenProvider<EmailConfirmationTokenProvider<AppTenantUser>>("emailConfirmation")
.AddTokenProvider<PasswordResetTokenProvider<AppTenantUser>>("passwordReset");
I had the same issue , after going throw the source code I realized that AddDefaultTokenProviders
method calls AddTokenProvider
which configures the IdentityOptions
and override the default token provider.
AddDefaultTokenProviders
public static IdentityBuilder AddDefaultTokenProviders(this IdentityBuilder builder)
{
var userType = builder.UserType;
var dataProtectionProviderType = typeof(DataProtectorTokenProvider<>).MakeGenericType(userType);
var phoneNumberProviderType = typeof(PhoneNumberTokenProvider<>).MakeGenericType(userType);
var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(userType);
var authenticatorProviderType = typeof(AuthenticatorTokenProvider<>).MakeGenericType(userType);
return builder.AddTokenProvider(TokenOptions.DefaultProvider, dataProtectionProviderType)
.AddTokenProvider(TokenOptions.DefaultEmailProvider, emailTokenProviderType)
.AddTokenProvider(TokenOptions.DefaultPhoneProvider, phoneNumberProviderType)
.AddTokenProvider(TokenOptions.DefaultAuthenticatorProvider, authenticatorProviderType);
}
AddTokenProvider
public virtual IdentityBuilder AddTokenProvider(string providerName, Type provider)
{
if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo()))
{
throw new InvalidOperationException(Resources.FormatInvalidManagerType(provider.Name, "IUserTwoFactorTokenProvider", UserType.Name));
}
Services.Configure<IdentityOptions>(options =>
{
options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider);
});
Services.AddTransient(provider);
return this;
}
and the method GenerateChangePhoneNumberTokenAsync
uses theIOptions<IdentityOptions>
which injected in the constructor of UserManager
so we can't use this method to generate tokens for multiple identities.
What I did
I used a method on the UserManager
called RegisterTokenProvider
to register my token provider,
and I made a new method in my user manager to generate the token, in this method I called GenerateUserTokenAsync
which takes the tokenProvider as a parameter.
CustomerUserManager
public class CustomerUserManager: UserManager<CustomerUser>
{
public CustomerUserManager(IUserStore<CustomerUser> store, IOptions<CustomerIdentityOptions> optionsAccessor,
IPasswordHasher<CustomerUser> passwordHasher, IEnumerable<IUserValidator<CustomerUser>> userValidators,
IEnumerable<IPasswordValidator<CustomerUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<GlameraUser>> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider<CustomerUser>());
}
public virtual Task<string> CustomChangePhoneNumberTokenAsync(CustomerUser user, string phoneNumber)
{
ThrowIfDisposed();
return GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, ChangePhoneNumberTokenPurpose + ":" + phoneNumber);
}
}
Note: I used AddDefaultTokenProviders
in the first identity only.
I'm not sure if this is the best way to solve this issue but it worked for me.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With