1

I want to implement email confirmation with digit code for my .net core web app. I make use of the GenerateEmailConfirmationTokenAsync from the UserManager class, but it returns a token instead of a digit code. I think I should create a custom implementation using the IUserTokenProvider interface, but I cant find any example.

How can I implement this using the IUserTokenProvider?

4
  • 1
    providing the code sample from your solution would be a good idea, otherwise it`s a wide Commented Apr 4, 2019 at 9:27
  • andrewlock.net/… Commented Apr 4, 2019 at 10:14
  • Do you want this digit code to be TOTP (Time-based One-time Password), i.e. do you want it to expire? Commented Apr 4, 2019 at 10:28
  • Yes, I do. I want it to expire Commented Apr 4, 2019 at 10:55

4 Answers 4

2

There is an existing implementation in ASP.NET Core Identity that does that.

The TotpSecurityStampBasedTokenProvider class (implementation of IUserTwoFactorTokenProvider interface) generates 6-digit tokens that expire.

TotpSecurityStampBasedTokenProvider is further inherited by EmailTokenProvider, but GenerateAsync method is reused from TotpSecurityStampBasedTokenProvider (not overridden).

These classes use user's security stamp to generate tokens, and provide their own modifiers, but the actual token generation is done by GenerateCode method from Rfc6238AuthenticationService class.

So, you can use the existing implementations:

  • TotpSecurityStampBasedTokenProvider
  • EmailTokenProvider (it does the same as the above one)
  • Rfc6238AuthenticationService if you really want to customize the token
Sign up to request clarification or add additional context in comments.

2 Comments

How can you make it return 4 digits? I've been trying to figure it out. I inherited from PhoneNumberTokenProvider<> and have overridden the GenerateAsync, but if you get the code from the base.GenerateAsync call you can't manipulate it because it is already stored. Where is the code stored? In memory? Should I just roll my own storage in the GenerateAsync and ValidateAsync methods?
As per usual, I post then come up with an answer on my own. I posted a gist of how I solved the problem of 4 digit token/codes. This could also be used to change the time window for validity to work with other services/use cases. gist.github.com/jmichas/dab35d9b8e916eae8936322465d76b66
1

You use that token to generate a action callback url

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
            var callbackUrl = Url.Action(
               "ConfirmEmail", "Account", 
               new { userId = user.Id, code = code }, 
               protocol: Request.Url.Scheme);

            await UserManager.SendEmailAsync(user.Id, 
               "Confirm your account", 
               "Please confirm your account by clicking this link: <a href=\"" 
                                               + callbackUrl + "\">link</a>");
            // ViewBag.Link = callbackUrl;   // Used only for initial demo.
            return View("DisplayEmail");
        }
        AddErrors(result);
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

https://learn.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity

3 Comments

Yes, I know. But i want send digit code (fro example "123468") instead link
Dont do that. Using MS solution is tested, secure, etc, etc.
Anders, the problem with generating an email with embedded URL is that you create an orphan window on the visitor browser which isn't ideal and confusing for many.
0

To build on Marko's answer;

We also have to tell Identity what token providers we want to use for the different kinds of tokens to make the UserManager-convenience methods like GenerateEmailConfirmationTokenAsync() or ConfirmEmailAsync() use a specific provider.

We do this by configuring the identity TokenOptions:

builder.Services.Configure<IdentityOptions>(options =>
{
    // Token settings.
    options.Tokens.EmailConfirmationTokenProvider = "MyCustomEmailTokenProvider";
    ...
});

Or together when registering the new provider using the fluent API:

services.AddDefaultIdentity<ApplicationUser>(options =>
    {
        options.Tokens.EmailConfirmationTokenProvider = "MyCustomEmailTokenProvider"
    })
    .AddTokenProvider<MyCustomEmailConfirmationTokenProvider>("MyCustomEmailTokenProvider");

The implementations Marko mentions and a few more providers gets added by default by .AddDefaultTokenProviders() that can be used without registering any additional providers by using their names:

  • TokenOptions.DefaultProvider, uses DataProtectorTokenProvider (the default provider used when no explicit configuration exist
  • TokenOptions.DefaultEmailProvider uses EmailTokenProvider
  • TokenOptions.DefaultPhoneProvider uses PhoneNumberTokenProvider
  • TokenOptions.DefaultAuthenticatorProvider uses AuthenticatorTokenProvider

Comments

0

For those folks who have read the replies (above) and are scratching their heads, I've thought I'd step through a solution.

I used the code from jmichas (see above) which worked "out of the box" in a .net.core project.

https://gist.github.com/jmichas/dab35d9b8e916eae8936322465d76b66#file-rfc6238authenticationservice-cs

I was using the default email token provider that returns a URL. There are lots of tuts out there advocating this method, but its main drawback is that it create an orphaned browser window. It also doesn't play well with a PWA work flow.

Step one, add your new classes from the link above. There was already a directory called TokenProvider in my project, so I created them in there.

  • FourDigitTokenProvider.cs
  • CustomRfc6238AuthenticationService.cs

Copy across the code from the url (above)

Step two, find your startup.cs file and I commented out the existing provider

// options.Tokens.EmailConfirmationTokenProvider = "emailconf";
// .AddTokenProvider<EmailConfirmationTokenProvider<ApplicationUser>>("emailconf")

And added the new ones...

options.Tokens.ChangePhoneNumberTokenProvider = FourDigitTokenProvider.FourDigitPhone; 
options.Tokens.EmailConfirmationTokenProvider = FourDigitTokenProvider.FourDigitEmail; 
.AddTokenProvider<FourDigitTokenProvider>(FourDigitTokenProvider.FourDigitEmail)
.AddTokenProvider<FourDigitTokenProvider>(FourDigitTokenProvider.FourDigitPhone)

Build and check it compiles.

Step three, I was using the default MVC controller to handle the validation. We don't really need this any more so I just created a new endpoint to handle the validation from my site. Something like this:

    [Produces("application/json")]
    [HttpGet]
    [Route("validateemailtoken/{token}/{email}")]
    public async Task<IActionResult> ValidateEmailToken(string token, string email)
    {
        var user = await _userManager.FindByEmailAsync(email);

        if (user != null)
        {
            if (user.EmailConfirmed)
            {
                return Ok(false);
            }

            var result = await _userManager.ConfirmEmailAsync(user, token);

            if (result.Succeeded)
            {
                user.IsEnabled = true;
                await _userManager.UpdateAsync(user);
                return Ok(result.Succeeded);
            } else
            {
                return Ok(false);
            }
        }

        return BadRequest(); 
    }

Step four, Ok, your user now will get an email with a four digit code, so you'll need your front end to input that code into your site (where they have been waiting for the email).

Grab the four digits and send it to your token server, so something like this...

 GET  http://localhost:xxxx/api/account/validateemailtoken/xxxx/[email protected]

It will return true if the user has been validated. You can log them in from there.

I hope all this helps.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.