0

I'm busy with the register functionality of my api and I have already implemented email confirmation which works fine. But there are a couple of problems that I am struggling with since I'm new to c# and .net core.

I want to implement a resend email functionality incase a user doesn't receive an email/doesn't accept in time, I have created a separate entity like this to store Confirmation tokens that .net generates by calling the UserManager.GenerateEmailConfirmationTokenAsync() method :

 public class ConfirmationToken {

        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string Token { get; set; }
        public string? ResendToken { get; set; }
        public bool? isUsed { get; set; }
        public DateTime? CreatedAt { get; set; } = DateTime.Now;
        public DateTime? ExpiredAt { get; set; } = DateTime.Now.AddHours(24);
        public virtual User? User { get; set; }
        public string UserId { get; set; }
    }

This does store the token that .net generates along with the specific user Id but there is an issue with the token it stores.

For example, when logging out the token provided by the GenerateEmailConfirmationTokenAsync() method I would get something like this after encoding it (This is what gets stored in my Confirmation Token table):

CfDJ8MhQpKYkLedOlUKK9%2bU10PcPTorIiCUSmUQ9vyl48rr91cCZnkYEuKKZLf4rLt90Dag6OWpkfbjwyhV1FbGNGuUZ90pUL7vi%2fe1T0JaXoV8SSiyZx41lWHwkb71rLTl3xYaDd6Bq6MaFClUjAEfyoorXZZ3K9ddmKb6Byf28%2bKRBtt2vlzayqxNkZbl43thYM%2fuzn8oCwkD8fc%2fhByV0wFCSgkUQKUHv3FKf5n%2bNbG3%2b%2bpwczzsooqoGvuyUSjsvqA%3d%3d

But after I call my confirm endpoint which in turn calls the UserManager.ConfirmEmailAsync method (I do pass my decoded token here and it does confirm properly as well as set EmailConfirmed=true) I get a different token, (this is the url that contains userId and the token to actually confirm the email) which looks like this:

https://localhost:7050/api/Auth/ConfirmEmail?userId=cc985a22-53dd-4e69-a0fe-70b82b6a9925&confirmationToken=CfDJ8MhQpKYkLedOlUKK9%252bU10PcPTorIiCUSmUQ9vyl48rr91cCZnkYEuKKZLf4rLt90Dag6OWpkfbjwyhV1FbGNGuUZ90pUL7vi%252fe1T0JaXoV8SSiyZx41lWHwkb71rLTl3xYaDd6Bq6MaFClUjAEfyoorXZZ3K9ddmKb6Byf28%252bKRBtt2vlzayqxNkZbl43thYM%252fuzn8oCwkD8fc%252fhByV0wFCSgkUQKUHv3FKf5n%252bNbG3%252b%252bpwczzsooqoGvuyUSjsvqA%253d%253d

So this token I get here is different from the one I store in my database, If I wanted to find a token by it's value I would never get any response because I always get a different version of the token already in my database.

I've been struggling for a while but I can't seem to understand why, the actual email confirmation does work, but now I want to add a resend email feature with a new token (and also validate that the old token is infact invalid etc).

My code is as follows:

RegisterUser() - Service method Registers the user account and also generates a link to confirm account

public async Task<User> RegisterUser(UserRegisterRequest request) {

            var userNameExists = await _userManager.FindByNameAsync(request.UserName);
            var emailExists = await _userManager.FindByEmailAsync(request.EmailAddress);

            var user = new User() {
                UserName = request.UserName,
                Email = request.EmailAddress,
                //EmailConfirmed = false
            };

            var result = await _userManager.CreateAsync(user, request.Password);

            if (result.Succeeded) {
                var confirmationToken = await _confirmationTokenService.GenerateConfirmationToken(user.Id);

               // Points to an endpoint to confirm the email and passes the userId and token
                var callback_url = "https://localhost:7050" + _urlHelper.Action("ConfirmEmail", "Auth",
                 new {
                     userId = confirmationToken.UserId,
                     confirmationToken = confirmationToken.Token
                 });

                return user;
            }

            return null;
        }

GenerateConfirmationToken(string userId) - Service method Generates a email token and inserts in DB along with userId

        public async Task<ConfirmationToken> GenerateConfirmationToken(string userId) {
            var user = await _userManager.FindByIdAsync(userId);

            var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);

            var encodedToken = HttpUtility.UrlEncode(token);

            var emailConfirmationToken = new ConfirmationToken {
                Token = encodedToken,
                UserId = user.Id,
                isUsed = true
            };

        // Save encoded token with associated userId to database table
            await _unitOfWork.ConfirmationTokens.Add(emailConfirmationToken);
            int result = _unitOfWork.Save();
        
            return emailConfirmationToken;
        }

ConfirmToken(string userId, string token) - Service method Confirms the decoded email token and userId being passed, this is used in the Register method ("/ConfirmEmail" is the endpoint calling this service method)

   public async Task<bool> ConfirmToken(string userId, string token) {
            var user = await _userManager.FindByIdAsync(userId);
            var decodedToken = HttpUtility.UrlDecode(token);
            var result = await _userManager.ConfirmEmailAsync(user, decodedToken);

            return result.Succeeded;
   }

ServletException
  • 171
  • 1
  • 15
  • why don't you search the token with userId instead of the token value? – Abdelkrim Jun 19 '23 at 17:14
  • But I would still need the proper confirmation token to be stored either way so that I can validate it when trying to resend an email and create a new token correct? – ServletException Jun 19 '23 at 17:22
  • Atm I have a test method to get the value by confirmation token, if I pass a token thats from my db i get null, but when I pass the token I get as a response from the callback_url then I get the actual token details that I need – ServletException Jun 19 '23 at 17:26
  • Ok, now I understand better. – Abdelkrim Jun 19 '23 at 17:33
  • I have answered you, let me know if that helped. – Abdelkrim Jun 19 '23 at 18:03
  • So that 3rd parties can't fill up your database, these tokens are usually math puzzles. Or encrypted data. The value would be based on a random value stored against the user, the current time, the type of token and probably a nonce value. Your server can create as many tokens as you want, without needing to store anything. – Jeremy Lakeman Jun 20 '23 at 01:01

1 Answers1

1

First of all, I think what you are trying to achieve is already implemented by Identity. But first let's try to fix what you have here and then let's try to explore the Identity solution.

Identifying the issue:

The root cause of your problem is that you are manually encoding the token: var encodedToken = HttpUtility.UrlEncode(token); This will cause problem because UrlHelper.Action will also encode it for the second time. So, your token which was like this :

CfDJ8MhQpKYkLedOlUKK9+U10PcPTorIiCUSmUQ9vyl48rr91cCZnkYEuKKZLf4rLt90Dag6OWpkfbjwyhV1FbGNGuUZ90pUL7vi/e1T0JaXoV8SSiyZx41lWHwkb71rLTl3xYaDd6Bq6MaFClUjAEfyoorXZZ3K9ddmKb6Byf28+KRBtt2vlzayqxNkZbl43thYM/uzn8oCwkD8fc/hByV0wFCSgkUQKUHv3FKf5n+NbG3++pwczzsooqoGvuyUSjsvqA==

Will be encoded by you, and it is the version that is stored in db and become like this:

CfDJ8MhQpKYkLedOlUKK9%2bU10PcPTorIiCUSmUQ9vyl48rr91cCZnkYEuKKZLf4rLt90Dag6OWpkfbjwyhV1FbGNGuUZ90pUL7vi%2fe1T0JaXoV8SSiyZx41lWHwkb71rLTl3xYaDd6Bq6MaFClUjAEfyoorXZZ3K9ddmKb6Byf28%2bKRBtt2vlzayqxNkZbl43thYM%2fuzn8oCwkD8fc%2fhByV0wFCSgkUQKUHv3FKf5n%2bNbG3%2b%2bpwczzsooqoGvuyUSjsvqA%3d%3d

Then when you pass it to _urlHelper.Action, will become like this :

CfDJ8MhQpKYkLedOlUKK9%252bU10PcPTorIiCUSmUQ9vyl48rr91cCZnkYEuKKZLf4rLt90Dag6OWpkfbjwyhV1FbGNGuUZ90pUL7vi%252fe1T0JaXoV8SSiyZx41lWHwkb71rLTl3xYaDd6Bq6MaFClUjAEfyoorXZZ3K9ddmKb6Byf28%252bKRBtt2vlzayqxNkZbl43thYM%252fuzn8oCwkD8fc%252fhByV0wFCSgkUQKUHv3FKf5n%252bNbG3%252b%252bpwczzsooqoGvuyUSjsvqA%253d%253d

So the quick fix, is to not encode the token yourself and store in the db directly (as it was generated), and when you receive it back in the confirmation side as part of the url, you don't need to decode it neither. So you can just check for equality.

Improving the current implementation:

Before sending the token as a route value to the confirmation page, it is recommended to base64 encode it (here is why)

This means that your code should be like this :

   if (result.Succeeded) {
            var confirmationToken = await _confirmationTokenService.GenerateConfirmationToken(user.Id);
            var base64EncodedToken = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(confirmationToken.Token))
            var callback_url = "https://localhost:7050" + _urlHelper.Action("ConfirmEmail", "Auth",
             new {
                 userId = confirmationToken.UserId,
                 confirmationToken = base64EncodedToken
             });

            return user;
        }

This of course will require you to do the opposite operation on the other side to get the proper Url :

var decodedUrl = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(token));

Using Identity :

That's said, Identity already offers a way to check if a token is valid or not, without you needing to store it in the db, retrieve and check different properties. The method that you can use is VerifyUserTokenAsync (if you decide to do the base64, which I highly recommend, you should decode from base64), then you can check if the token is valid or not:

var isTokenValid = await VerifyUserTokenAsync(user, _userManager.Options.Tokens.EmailConfirmationTokenProvider, "EmailConfirmation", token)

This will give you if the token is valid or not, without you needing to have a handcrafted solution and it won't automatically mark the email as valid in the user db (contrary to _userManager.ConfirmEmailAsync(user, code)).

Abdelkrim
  • 1,083
  • 8
  • 18