prevent users without confirmed email from logging

2019-03-15 06:20发布

问题:

In microsoft Identity 2 there is ability to users can confirm there email addresses I downloaded Identity 2 sample project from here in this project there isn't any difference between users confirmed their emails and who doesn't I want to people how don't confirmed their emails can't login this is what I tried :

public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }


        var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: true);
        switch (result)
        {
            case SignInStatus.Success:
                {


                    var user = await UserManager.FindByNameAsync(model.Email);
                    if (user != null)
                    {
                        if (!await UserManager.IsEmailConfirmedAsync(user.Id))
                        {
                            //first I tried this.
                            //return LogOff();
                            HttpContext.Server.TransferRequest("~/Account/LogOff");
                            return RedirectToAction("Login");
                        }
                    }

                    return RedirectToLocal(returnUrl);
                }
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
            case SignInStatus.Failure:
            default:
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);

I tried to force user to Logoff by calling LogOff() action method but It didn't work and user remain authenticated .then I tried to use Server.TransferRequest() but I don't know why it did the job but it redirects users to login page with returnUrl="Account/Logoff" so after they confirmed their email and tried to login they get logoff I get really confused!! this is my LogOff() action method:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult LogOff()
    {
        AuthenticationManager.SignOut();
        return RedirectToAction("About", "Home");
    }

I have googled it for days without any luck !!!!

回答1:

Maybe its a little late but I hope it may help others.

Add this

var userid = UserManager.FindByEmail(model.Email).Id;
        if (!UserManager.IsEmailConfirmed(userid))
        {
            return View("EmailNotConfirmed");
        }

before

var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);

The first block of code just checks if the email in the model exists in the database and gets it's id to check if it is not confirmed and if so returns a view to the user wich says so and if it is confirmed just lets the user sign in.

And delete your changes to the result switch like this

switch (result)
        {
            case SignInStatus.Success:
                    return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
            case SignInStatus.Failure:
            default:
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);
        }


回答2:

Instead of moving to another page, why not finish this one and redirect to the right action / view:

if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
    return RedirectToAction("ConfirmEmailAddress", new { ReturnUrl = returnUrl });
}

You do need an action (and possibly a view) with the name ConfirmEmailAddress though.



回答3:

I would let the admin create the user without any password. The email with link should go to the user. The user then is directed to SetPassword page to set new password. This way no one can access the user account unless he confirms and sets the password.

Call CreateAsync without the password

var adminresult = await UserManager.CreateAsync(user);

Redirect admin to new custom view saying something like "Email is sent to user"

@{
    ViewBag.Title = "New User created and Email is Sent";
}
<h2>@ViewBag.Title.</h2>
<p class="text-info">
    The New User has to follow the instructions to complete the user creation process.
</p>
<p class="text-danger">

    Please change this code to register an email service in IdentityConfig to send an email.
</p>


回答4:

There is a solution, which may not be the best approach, but it works. First let me try to clarify why your approach did not work.

In one of the comments it is mentioned, the AuthenticationManager uses cookies. In order to update a cookie you need to send it to the client, using another page. That is why TransferRequest is not going to work.

How to handle the emailverification? The strategy I used:

1) On SignInStatus.Success this means that the user is logged in.

2) When email is not confirmed: send an email to the used e-mailaddress. This is safe since the user already signed in. We are just blocking further access until the e-mail is verified. For each time a user tries to login without having validated the email, a new email (with the same link) is sent. This could be limited by keeping track of the number of sent emails.

3) We cannot use LogOff: this is HttpPost and uses a ValidateAntiForgeryToken.

4) Redirect to a page (HttpGet, authorization required) that displays the message that an e-mail has been sent. On entering sign out the user.

5) For other validation errors, redirect to another method to sign out (HttpGet, authorization required). No view needed, redirect to the login page.

In code: update the code in AccountController.Login to:

        case SignInStatus.Success:
        {
            var currentUser = UserManager.FindByNameAsync(model.Email);
            if (!await UserManager.IsEmailConfirmedAsync(currentUser.Id))
            {
                // Send email
                var code = await UserManager.GenerateEmailConfirmationTokenAsync(currentUser.Id);
                var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = currentUser.Id, code = code}, protocol: Request.Url.Scheme);
                await UserManager.SendEmailAsync(currentUser.Id, "Confirm your account", string.Format("Please confirm your account by clicking this link: <a href=\"{0}\">link</a>", callbackUrl));
                // Show message
                return RedirectToAction("DisplayEmail");
            }
            // Some validation
            if (true)
            {
                return RedirectToAction("SilentLogOff");
            }
            return RedirectToLocal(returnUrl);
        }

Add methods to AccountController:

// GET: /Account/SilentLogOff
[HttpGet]
[Authorize]
public ActionResult SilentLogOff()
{
    // Sign out and redirect to Login
    AuthenticationManager.SignOut();
    return RedirectToAction("Login");
}

// GET: /Account/DisplayEmail
[HttpGet]
[Authorize]
public ActionResult DisplayEmail()
{
    // Sign out and show DisplayEmail view
    AuthenticationManager.SignOut();
    return View();
}

DisplayEmail.cshtml

@{
    ViewBag.Title = "Verify e-mail";
}

<h2>@ViewBag.Title.</h2>
<p class="text-info">
    Please check your email and confirm your email address.
</p>

You'll notice that the user cannot reach other pages until email is verified. And we are able to use the features of the SignInManager.

There is one possible problem (that I can think of) with this approach, the user is logged in for the time that the email is sent and the user is being redirected to the DisplayMessage view. This may not be a real problem, but it shows that we are not preventing the user from logging in, only denying further access after logging in by automatically logging out the user.

=== Update ====

Please note that exceptions have to be handled properly. The user is granted access and then access is revoked in this scenario. But in case an exception occurs before signing out and this exception was not catched, the user remains logged in.

An exception can occur when the mailserver is not available or the credentials are empty or invalid.

===============



回答5:

The answer by @INFINITY_18 may cause Object reference not set to an instance of an object error if the email does not exist in the data store at all. And why not return the Login view with model error in this case, too?

I would suggest the following:

var userid = UserManager.FindByEmail(model.Email)?.Id;
if (string.IsNullOrEmpty(userid) || !UserManager.IsEmailConfirmed(userid)))
{
    ModelState.AddModelError("", "Invalid login attempt.");
    return View(model);
}