No IUserTwoFactorTokenProvider<TUser> named 'Default' is registered

Viewed 2710

I am working on an asp.net core mvc project (.net 5) but struggling hard with Identity related things.

I have a class HedgehogUserAccount that inherits from IdentityUser with two other classes that inherit from HedgehogUserAccount: CustomerAccount and UserAccount (bad choice of name, I know - I will change it when everything works). After a lot of work, I have managed to get the migrations to work and the program compiles and runs, but when I try to register a User I get the following error:

NotSupportedException: No IUserTwoFactorTokenProvider<TUser> named 'Default' is registered.

The error comes from line 90 in my registration code (see full code pasted below - it is almost the same as the scaffolded registration page):

var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);

The relevant (I think) part of my Startup.cs looks like this:

services.AddIdentity<HedgehogUserAccount, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders()
        .AddDefaultUI();

services.AddIdentityCore<UserAccount>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders()
        .AddDefaultUI();

services.AddIdentityCore<CustomerAccount>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders()
        .AddDefaultUI();

I arrived at this code when trying to register my UserManager classes.

I have tried some variations of this (replacing the AddDefaultTokenProviders):

  1. .AddTokenProvider(TokenOptions.DefaultEmailProvider, typeof(EmailTokenProvider<UserAccount>))
  2. .AddTokenProvider<EmailTokenProvider<UserAccount>>(TokenOptions.DefaultEmailProvider)

But both of them yield the same error at the same point in my code. But I got an interesting message when I tried:

services.AddIdentityCore<UserAccount>(
      options => {
            options.Tokens.ProviderMap.Add("Default", new 
                  TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<UserAccount>)));
                 })
      .AddRoles<IdentityRole>()
      .AddEntityFrameworkStores<ApplicationDbContext>()
      .AddDefaultTokenProviders()
      .AddDefaultUI();

Then the error message I get is: System.ArgumentException: 'An item with the same key has already been added. Key: Default' which feels a bit weird since the previous message said that no Default exists.

I guess what I must do is to provide my own EmailTokenProvider somehow? Or is there any other way? I would very much appreciate your help!

Update

I tried replace the line where the confirmation token is generated with the following line:

var code = "testtoken";//await _userManager.GenerateEmailConfirmationTokenAsync(user);

When I do this the program and the registration runs flawlessly! I guess this means that somehow the default token provider is not being registered correctly, but I am unsure how to proceed.


Register.cshtml.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Hedgehog.Core.Application;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;

namespace Hedgehog.UI.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<UserAccount> _signInManager;
        private readonly UserManager<UserAccount> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly ILogger<RegisterModel> _logger;
        private readonly IEmailSender _emailSender;

        public RegisterModel(
            UserManager<UserAccount> userManager,
            RoleManager<IdentityRole> roleManager,
            SignInManager<UserAccount> signInManager,
            ILogger<RegisterModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = new UserAccount { UserName = Input.Email, Email = Input.Email };
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    _userManager.AddToRoleAsync(user, "User").Wait();

                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    }
                    else
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }
    }
}
2 Answers

The solution that works for me is:
Add .AddDefaultTokenProviders(); only to one of your IdentityDbContext, then on the other 2, Create a custom UserManager that inherits from UserManage then in the constructor add the following line

RegisterTokenProvider(TokenOptions.DefaultProvider, new EmailTokenProvider<Owner>());

this line registers a new EmailTokenProvider for the UserManager.

After a lot of headaches, I have now reached a conclusion to my problem. I solved it by implementing my own token provider and registering it with the system. In case anyone else crashes into the same wall as I did, I leave my solution here.

Step 1: Implement IUserTwoFactorTokenProvider

public class HedgehogEmailTwoFactorAuthentication<TUser> : IUserTwoFactorTokenProvider<TUser> where TUser : HedgehogUserAccount
    {
        public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user)
        {
            if (manager != null && user != null)
            {
                return Task.FromResult(true);
            }
            else
            {
                return Task.FromResult(false);
            }
        }

        // Genereates a simple token based on the user id, email and another string.
        private string GenerateToken(HedgehogUserAccount user, string purpose)
        {
            string secretString = "coffeIsGood";
            return secretString + user.Email + purpose + user.Id;
        }

        public Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
        {
            return Task.FromResult(GenerateToken(user, purpose));
        }

        public Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
        {
            return Task.FromResult(token == GenerateToken(user, purpose));
        }
    }

Step 2: Register your new and shiny token provider in services

services.AddIdentityCore<CustomerAccount>(options => options.SignIn.RequireConfirmedAccount = true)
                    .AddRoles<IdentityRole>()
                    .AddEntityFrameworkStores<ApplicationDbContext>()
                    .AddTokenProvider("Default", typeof(HedgehogEmailTwoFactorAuthentication<CustomerAccount>))
                    .AddDefaultUI();

Step 3: Register with DI container

In ConfigureContainer (I'm using Autofac):

builder.RegisterType<HedgehogEmailTwoFactorAuthentication<CustomerAccount>>()
                    .As<IUserTwoFactorTokenProvider<CustomerAccount>>()
                    .InstancePerLifetimeScope();

If you are not using Autofac, you need to call AddScoped instead of InstancePerLifetimeScope (assuming this is the correct lifetime for a token generator).

Also, you might want to use a better token generator for your implementation :)

Reference: Microsoft documentation

Related