I am making api in .net 5 and implementing sending email confirmation after succesful registration. This one is killing me and can’t see the reason why it returns Invalid Token
.
So here’s my code:
Startup.cs
services.AddAuthentication
...
...
...
services.AddIdentityCore<User>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
}).AddEntityFrameworkStores<WorkoutDbContext>()
.AddDefaultTokenProviders();
EmailHelper.cs
public class EmailHelper
{
public bool SendEmail(string userEmail, string confirmationLink)
{
MailMessage mailMessage = new MailMessage();
mailMessage.From = new MailAddress("noreply@*********.io");
mailMessage.To.Add(new MailAddress(userEmail));
mailMessage.Subject = "Confirm your email";
mailMessage.IsBodyHtml = false;
mailMessage.Body = confirmationLink;
SmtpClient client = new SmtpClient();
client.UseDefaultCredentials = false;
client.Credentials = new System.Net.NetworkCredential("noreply@*********.io", "super secret password");
client.Host = "host";
client.Port = 000;
client.EnableSsl = false;
client.DeliveryFormat = (SmtpDeliveryFormat)SmtpDeliveryMethod.Network;
try
{
client.Send(mailMessage);
return true;
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
// log exception
}
return false;
}
}
AccountService.cs
public async void RegisterUser(RegisterUserDto dto)
{
var uniqueId = Guid.NewGuid().ToString()[..8];
var newUser = new User()
{
Name = dto.Name,
LastName = dto.LastName,
Email = dto.Email,
EmailConfirmed = false,
UniqeId = uniqueId,
RegistrationDate = DateTime.Now,
Active = false,
RoleId = dto.RoleId
};
var hashedPassword = _passwordHasher.HashPassword(newUser, dto.Password);
newUser.Password = hashedPassword;
newUser.NormalizedEmail = dto.Email;
_context.Add(newUser);
_context.SaveChanges();
var user = await _userManager.FindByEmailAsync(newUser.Email);
if(user != null)
{
EmailHelper emailHelper = new EmailHelper();
var token = HttpUtility.UrlEncode(await _userManager.GenerateEmailConfirmationTokenAsync(newUser));
var confirmationLink = "http://localhost:5000/confirmEmail?token=" + token + "&email=" + newUser.NormalizedEmail;
bool emailResponse = emailHelper.SendEmail(newUser.NormalizedEmail, confirmationLink);
}
}
User registration works fine. If email is not registered than registration fails. Otherwise server return 200 as expected. Confirmation link is sent to email provided during registration process. So till now works as expected.
EmailController.cs
[HttpGet]
public async Task<IdentityResult> ConfirmEmail([FromQuery]string token, [FromQuery] string email)
{
var user = await _userManager.FindByEmailAsync(email);
var result = await _userManager.ConfirmEmailAsync(user, token);
if (result.Succeeded) { return IdentityResult.Success; }
else { return (IdentityResult) result; }
}
Problem start when clicking on activation link – it throws warn: Microsoft.AspNetCore.Identity.UserManager[9] VerifyUserTokenAsync() "code": "InvalidToken",
So activation link is sent, it checks if user !== null
and than goes to conditional statement and triggers EmailHelper which sends link to inbox correctly.
…BUT
When I try to console user – it’s empty, nothing shows in console.
Database is updated and user is saved in DB and it’s visible and accessible.
In EmailController.cs it does not return user
var user = await _userManager.FindByEmailAsync(email);
// if I try to console it it shows nothing - empty(not NULL)
If I try to send serialized user data to inbox instead of sending activation link it sends correct data from DB.
So to sum up:
- user is created and saved in DB,
- sending user data to inbox instead of confirmationLink sends accurate and correct data of user from database and proves that everything went ok
- confirmation link is generated correctly and send to inbox
- checking
var user = await _userManager.FindByEmailAsync(newUser.Email); if(user != null)
makes user NOT null send allows to send email
And this is killing me and do not know why checking if user exists gives true but trying to access via await _userManager.FindByEmailAsync(newUser.Email);
gives no user(nothing)?
I tried also (in EmailController) _context.Users.FirstOrDefault(u => u.NormalizedEmail == email)
but the result is the same – cannot get user from DB and returns
Microsoft.AspNetCore.Identity.UserManager[9]
VerifyUserTokenAsync() failed with purpose: EmailConfirmation for user.
UPDATE
IdentityResult output
{
"succeeded": false,
"errors": [
{
"code": "InvalidToken",
"description": "Invalid token."
}
]
}
UPDATE #2
When I serialize user
and try to display in console it actually shows correct user data as a string. The problem seems to be that neither
var user = await _userManager.FindByEmailAsync(email);
nor var user= _context.Users.FirstOrDefault(u => u.Email == email);
does not return user that can be verified.
UPDATE #3
No IUserTwoFactorTokenProvider<TUser> named 'xxxx' is registered. System.NotSupportedException: No IUserTwoFactorTokenProvider<TUser> named 'xxxx' is registered.
But AddDefaultTokenProviders()
was implemented in Startup.cs
Even tried config.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
or
config.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<User>)));
but the result is the same.
2
Answers
I was working on the solution for past few days but finally found a solution!
Since I am using
IdentityUser
but myUser
model did not contain table columns from Identity it cause problem, most likely because that security stamp column was not present.What I did was to include table columns from
IdentityUser
.I have also included
Also in EmailController.cs in
ConfirmEmail
I had todecode
token because I wasencoding
this withHttpUtility.Encode
when egenrating.var tokenDecode = HttpUtility.UrlDecode(token);
Than I checked with
VerifyUserTokenAsync
which returnedtrue
. After that I only received confirmation error when email was clicked thatUsername '' is invalid. Username can have only letters and number.
What I also did was generating
userName
containing only letters and numbers from email that was used for registration and assigning it toUserName
column in DB set.After that when email was sent and clicked it returned
IdentityResult.Success
Damn! It was a long way home. :)
In my .NET core 6, encode send via email:
Then you should decode token before confirm: