Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No IUserTwoFactorTokenProvider named 'Default' is registered. Problem is AddDefaultTokenProviders() in two (2) ASP.NET Core Identity registrations

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.

like image 424
Ronnie Avatar asked Dec 12 '19 11:12

Ronnie


3 Answers

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()

like image 123
Shawn de Wet Avatar answered Oct 23 '22 15:10

Shawn de Wet


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");
like image 7
hakantopuz Avatar answered Oct 23 '22 14:10

hakantopuz


I had the same issue , after going throw the source code I realized that AddDefaultTokenProviders method calls AddTokenProvider which configures the IdentityOptionsand 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 the
IOptions<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.

like image 5
Ibrahim Mohamed Avatar answered Oct 23 '22 13:10

Ibrahim Mohamed