dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.58k stars 25.29k forks source link

Debug hints on Account confirmation and password recovery #15830

Open alwilton opened 4 years ago

alwilton commented 4 years ago

Please add the fact that in order for a forgot password email to be sent the user must #1 exist in the DB and #2the user must have EmailConfirmed Set to True. Also, if that is not the case there is no indication of that via the web interface, the email is never attempted to be sent.

I would like like to suggest that if env.IsDevelopment() is true that feedback be provided these requirments. - Thx


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

Rick-Anderson commented 4 years ago

#1 is obvious but #2 might be worth mentioning.

jesorian commented 4 years ago

I followed this tutorial on .net core 3.1

and whatever I do the email is not sending.

I debugged the application and it really execute the Task Execute method but, I did not received any using forgot password and verify account. I also checked the SendGrid stats it's always 0.

Anyone can enlighten me with this would be much appreciated.

ardalis commented 4 years ago

The issue is twofold:

If you debug and break on the email send, you should be able to get the email to send. But if you just run it, the thread will end before the request is sent due to the missing await.

This code is working for me and includes the missing awaits as well as a check for the response to be successful:

        public async Task SendEmailAsync(string email, string subject, string message)
        {
            var response = await Execute(Options.SendGridKey, subject, message, email);

            if(response.StatusCode != System.Net.HttpStatusCode.Accepted)
            {
                // log or throw
                throw new Exception("Could not send email: " + await response.Body.ReadAsStringAsync());
            }
        }

        private async Task<Response> Execute(string apiKey, string subject, string message, string email)
        {
            var client = new SendGridClient(apiKey);
            var msg = new SendGridMessage()
            {
                From = new EmailAddress("admin@DevBetter.com", "Admin"),
                Subject = subject,
                PlainTextContent = message,
                HtmlContent = message
            };
            msg.AddTo(new EmailAddress(email));

            // Disable click tracking.
            // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
            msg.SetClickTracking(false, false);

            return await client.SendEmailAsync(msg);
        }
Rick-Anderson commented 4 years ago

@ardalis do you want to open an issue on the template?

PeterWone commented 4 years ago

For me, password reset emails work perfectly but the confirmation message is never sent. I have a breakpoint in the start of the SendEmailAsync method and it's hit every time for password reset but never for confirmation.

NoelStephensNILC commented 4 years ago

I actually placed a break point in SendEmailAsync on the call to the Execute method, on the construction of the SendGridClient, and on "return client.SendEmailAsync();". None of these are being called... I have checked to make sure that this: // Sets up email confirmation services.AddTransient<IEmailSender, EmailSender>(); services.Configure(Configuration); services.AddRazorPages(); is within the configure services method as well. What would be the steps in figuring out why the SendEmailAsync is not being called?

NoelStephensNILC commented 4 years ago

Ok, I figured out my issue...but this very well could be a bug.

If you add an external authentication method (i.e. Microsoft Authentication), when you click on register and are presented with the registration page you can register with the external authentication method (in my case Microsoft Authentication) as opposed to creating a local account. If you use external authentication as your registration method, then the email confirmation process is completely bypassed/ignored.

Is this "as by design" or is this a bug?
If it is a bug, then does anyone have advice on the best way to invoke the email confirmation and/or auto-verify (confirm) the account once the external authentication process has completed with success?

NoelStephensNILC commented 4 years ago

Forgot to mention... even though the email confirmation process is never invoked (i.e. SendEmailAsync is never called) when you register using an external authorization as opposed to local, upon completing the external authentication process successfully it will place the user back on the "Register Confirmation" page.

So, either it should "auto confirm" upon success of the external authentication process and proceed to the confirmation complete page =or= it should invoke the SendEmailAsync method.

Any thoughts are appreciated! :)

PeterWone commented 4 years ago

I worked the exercise a second time with success. Here are the details of the entire exercise on Stack Overflow. https://stackoverflow.com/questions/60617079/core3-react-confirmation-email-not-sent

NoelStephensNILC commented 4 years ago

Well, I am using SendGrid and am assuming you were still using your exchange server when you worked through it again? I too started from scratch (starting with the very beginning of this example and then adding the external authentication afterwards). So, there was no 3.0 to 3.1 back and fourth in my case. Just the example, SendGrid, and Microsoft Authentication. Maybe I missed a step, but I already started a new project from scratch and everything so far looks identical...will pickup on this tomorrow and see if the next version I am working through works. Although, it is a bit unnerving that you could make changes to your solution (even if just switching between frameworks) and it could completely break the authentication process...with no real way (other than building .NETCore and debugging through that) to determine what is "broken". Also, last time I installed the JDK on one of my development machines (about 12 years ago) it got infected with a virus (the first and only time I had a system get compromised)...so not too keen on having to install the JDK I order to build debug binaries in order to debug something that should already be "debuggable".
Doesn't look like they offer debug builds on GitHub. Would be great if you could get debug builds of the current version so you could still debug through but not have to build it (and install all the required tools/sdks).

Will let you know how my second pass goes.

PeterWone commented 4 years ago

Solve one problem at a time. DummyEmailSender from my SO question that you have obviously read does nothing but meet the interface contract, and log the fact that SendEmailAsync is called. Use that and see whether you can get correct behaviour.

PeterWone commented 4 years ago

Read this, learn to scaffold, then debug.

https://stackoverflow.com/questions/60739857/how-to-customise-or-style-the-auth-ux-in-dotnet-core-3

NoelStephensNILC commented 4 years ago

Thank you Peter! The Scaffold path was exactly what I needed. I am ramping back up with all of the ASP/Razor/NET Core Web Application stuff that has changed over the past 7-8 years (I was primarily writing network drivers during that time). Either case, I went ahead and generated all of the RTL source for Identity and I believe I now know what is the issue with External Authentication. Within the ExternalLogin.cshtml.cs file you will find the OnPostConfirmationAsync method and within that method this is the code of interest with the suspect culprit in bold:

          var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
            var result = await _userManager.CreateAsync(user);
            if (result.Succeeded)
            {
                result = await _userManager.AddLoginAsync(user, info);
                if (result.Succeeded)
                {
                    _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                    // If account confirmation is required, we need to show the link if we don't have a real email sender
                      **if (_userManager.Options.SignIn.RequireConfirmedAccount)
                      {
                          return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                      }**
                    await _signInManager.SignInAsync(user, isPersistent: false);
                    var userId = await _userManager.GetUserIdAsync(user);
                    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 = userId, code = code },
                        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>.");

                    return LocalRedirect(returnUrl);
                }
            }

What I believe is happening is that the SignIn.RequireConfirmedAccount is true and it bypasses sending any confirmation email and just redirects the user directly to the RegisterConfirmation page. This is why (I believe) using external authentication as your registration method will dump the user to the "Registration Confirmation" page stating an email has been sent but in reality it never gets sent. The first time I debugged it, I let it naturally progress and noted that once it redirects to the register confirmation page it never calls SendEmailAsync. The 2nd time I debugged it, I went ahead and manually jumped pass that SignIn.RequireConfirmedAccount check and while it did send the email it just returned me back to the default/home page.
My best guess would be if I were to remove this:

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                    }

And then add it back towards the end of that conditional branch:

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                    }
                    else
                    {
                        return LocalRedirect(returnUrl);
                    }

Then it would send the email & push the user to the Register Confirmation page.

Any thoughts on this? (Good/Bad Idea)?

Either case, thank you for your tips/guidance! Once I ran through the Scaffold Identity article, everything "clicked into place"...

Cheers,

Noel

NoelStephensNILC commented 4 years ago

For anyone else who finds themselves reading through these threads here are some additional modifications to the above changes to fix some issues with whether the user should be logged in or not during the registration process.

I added an additional call to sign out the externally authenticated user in order to assure that it doesn't keep the user logged in while still waiting for authentication:

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        await _signInManager.SignOutAsync();
                        return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                    }
                    else
                    {
                        return LocalRedirect(returnUrl);
                    }

Additionally, in the same ExternalLogin.cshtml.cs file I also modified the OnGetCallbackAsync method with this:

            // If the user does not have an account, then ask the user to create an account.
            ReturnUrl = returnUrl;
            LoginProvider = info.LoginProvider;
            if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
            {
                Input = new InputModel
                {
                    Email = info.Principal.FindFirstValue(ClaimTypes.Email)
                };
                // Check for a user account waiting to be confirmed via email
                var userinfo = await _userManager.FindByNameAsync(Input.Email);
                if (userinfo != null && !userinfo.EmailConfirmed)
                {
                    // User hasn't confirmed yet, so just push them to the register confirmation page
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }
            }

While I am sure there are much better ways to handle user login states, this is just a heads up for anyone else that ran into similar issues (starting with the issue of email confirmations for users that register with an external authentication source). The two above changes assures that the user is not considered "logged in" until they have confirmed their email address as well as assures that if the user returns without confirming it will see they have registered but not confirmed and will just display the "Register Confirmation" page.

Anyway, thank you Peter for pointing me in the right direction... going to start reading up on additional materials regarding user authentication states and management of them from page to page (etc).

adnanyangilic commented 4 years ago

Where can we possibly find latest version typical implementation examples of ASP.Net Core Identity razor pages? ASP.Net Core 3.1 Identity all pages in Areas/Identity for example.. New learners need those examples but things are changing fast and teacher candidates keep being students in this fast change.