Block Login If Email Not Confirmed in ASP.NET Core Identity

How to Block Login if Email is not Confirmed in ASP.NET Core Identity

In this article, I will discuss How to Block Login if Email is not Confirmed in ASP.NET Core Identity. Please read our previous article discussing Email Confirmation in ASP.NET Core Identity.

How do you block login if email is not confirmed in ASP.NET Core Identity?

To block login if the email is not confirmed in an ASP.NET Core Identity application, you will need to modify the login logic to check whether the user’s email address has been confirmed. Here’s a step-by-step guide on how to implement this:

  • Modify the Login Action: In your Account Controller, locate the action that handles the login request. This is usually the Login action in the Account Controller.
  • Check Email Confirmation: After you validate the user’s credentials but before you sign them in, check if their email address has been confirmed.
  • Reject Login if Not Confirmed: If the user’s email is not confirmed, do not proceed with the login process. Instead, you can redirect them to a page informing them to confirm their email or resend the confirmation email.
  • Resend Confirmation Email: Provide an option for the user to resend the confirmation email if they haven’t received it.
Example Flow:

If the email address is not confirmed and the user tries to log in, we want to display the validation error – Email not confirmed yet, as shown in the image below. Further, if you notice, we provide a Confirm Email button to confirm the user’s email address.

How to Block Login if Email is not Confirmed in ASP.NET Core Identity

In ASP.NET Core Identity, application users are stored in the AspNetUsers database table. The EmailConfirmed column in this table determines if a given email address is confirmed.

How to Block Login if Email is not Confirmed in ASP.NET Core Identity

How to Configure Require Confirm Email?

We need to Configure the Require Confirm Email in the Program class where we configure the Identity Services. We need to set the RequireConfirmedEmail property to true, as shown in the below image.

How to Block Login if Email is not Confirmed in ASP.NET Core Identity

Suppose the RequireConfirmedEmail property is set to true, and the email address is not yet confirmed. If we now use the SignInManager service PasswordSignInAsync() method to sign in, the user will get NotAllowed as a result, even if we supply the correct username and password.

The same is true with the ExternalLoginSignInAsync() method of the SignInManager service. We use the ExternalLoginSignInAsync() method to sign-in the user using an external login provider like Facebook, Google, Microsoft, Twitter, etc. If the email address associated with the external login account is not confirmed, the sign-in result will be NotAllowed.

Modifying Login View:

Please modify the Login.cshtml view as follows. Here, we are adding the Confirm Email button. Clicking the Confirm Email button will redirect to the ResendConfirmationEmail action method of the Account Controller.

@model LoginViewModel

<div class="row">
    <div class="col-md-6">
        <h1>Local Account Login</h1>
        <hr />
        <form method="post" asp-action="Login" asp-controller="Account">
            <input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <div class="checkbox">
                    <label asp-for="RememberMe">
                        <input asp-for="RememberMe" />
                        @Html.DisplayNameFor(m => m.RememberMe)
                    </label>
                </div>
            </div>
            <div class="form-group">
                <button type="submit" class="btn btn-primary">Login</button>
            </div>
            <br />
            <div class="form-group">
                <a class="btn btn-primary" asp-controller="Account"
                   asp-action="ResendConfirmationEmail">Confirm Email</a>
            </div>

        </form>
    </div>
    <div class="col-md-6">
        <h1>External Login</h1>
        <hr />
        @{
            if (Model.ExternalLogins == null || Model.ExternalLogins?.Count == 0)
            {
                <div>No external logins configured</div>
            }
            else
            {
                <form method="post" asp-action="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl">
                    <div>
                        @foreach (var provider in Model.ExternalLogins)
                        {
                            <button onclick="externalLogin(@provider.Name, @Model.ReturnUrl)" type="submit" class="btn btn-primary"
                                    name="provider" value="@provider.Name"
                                    title="Log in using your @provider.DisplayName account">
                                @provider.DisplayName
                            </button>
                        }
                    </div>
                </form>
            }
        }
    </div>
</div>
Modifying the Login Post Action method

Let us modify the Login Post Action method of the Account Controller as follows. Here, we are adding the IsNotAllowed catch block, which will be executed when the Email ID is not confirmed.

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model, string? ReturnUrl)
{
    //If Model Login Failed, we also need to show the External Login Providers 
    model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

    if (ModelState.IsValid)
    {
        var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);

        if (result.Succeeded)
        {
            // Handle successful login

            // Check if the ReturnUrl is not null and is a local URL
            if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl))
            {
                return Redirect(ReturnUrl);
            }
            else
            {
                // Redirect to default page
                return RedirectToAction("Index", "Home");
            }
        }
        if (result.RequiresTwoFactor)
        {
            // Handle two-factor authentication case
        }
        else if (result.IsLockedOut)
        {
            // Handle lockout scenario
        }
        else if (result.IsNotAllowed)
        {
            ModelState.AddModelError(string.Empty, "Email not confirmed yet");
            return View(model);
        }
        else
        {
            // Handle failure
            ModelState.AddModelError(string.Empty, "Invalid Login Attempt.");
            return View(model);
        }
    }

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

Now, run the application and test it, and it should work as expected. In this case

Problem with the above Implementation:

The problem with the above implementation is that it first checks whether the Email ID is confirmed or not, and if it is confirmed, then it checks whether the provided correct username and password match or not.

We want to display the Email not confirmed yet” error message only if the Email is not confirmed AND the user has provided the correct username and password.

Suppose you are wondering why we need to check if the user has provided the correct username and password. Well, this is to avoid account enumeration and brute force attacks.

Let’s understand what might happen if we display this validation message – Email not confirmed yet, without checking if the provided email address and password combination is correct. An attacker might try random emails, and as soon as he sees the message, Email not confirmed yet, he knows this is a valid email that could be used to log in. He waits a couple of days until the owner confirms the email and can try random passwords with that email address to gain access.

In order to avoid these types of account enumeration and brute force attacks, display the validation error only upon providing the correct email address and password combination.

So, what we want is first to check whether the email and password are correct or not, and then we need to check whether the email is confirmed or not. To do so, we need to fetch the user details based on the Email ID manually, and then we need to check whether the Email ID is confirmed and the Password is correct. To do so, modify the Login Post Action Method of the Account Controller as follows. Here, we have also removed the NotAllowed try block.

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model, string? ReturnUrl)
{
    //If Model Login Failed, we also need to show the External Login Providers 
    model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

    if (ModelState.IsValid)
    {
        //First Fetch the User Details by Email Id
        var user = await userManager.FindByEmailAsync(model.Email);

        //Then Check if User Exists, EmailConfirmed and Password Is Valid
        //CheckPasswordAsync: Returns a flag indicating whether the given password is valid for the specified user.
        if (user != null && !user.EmailConfirmed &&
                    (await userManager.CheckPasswordAsync(user, model.Password)))
        {
            ModelState.AddModelError(string.Empty, "Email not confirmed yet");
            return View(model);
        }

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

        if (result.Succeeded)
        {
            // Handle successful login

            // Check if the ReturnUrl is not null and is a local URL
            if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl))
            {
                return Redirect(ReturnUrl);
            }
            else
            {
                // Redirect to default page
                return RedirectToAction("Index", "Home");
            }
        }
        if (result.RequiresTwoFactor)
        {
            // Handle two-factor authentication case
        }
        if (result.IsLockedOut)
        {
            // Handle lockout scenario
        }
        else
        {
            // Handle failure
            ModelState.AddModelError(string.Empty, "Invalid Login Attempt.");
            return View(model);
        }
    }

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

With the above changes in place, the Email not confirmed yet error message is displayed only if the Email is not confirmed AND the user has provided the correct username and password. Otherwise, it will display an Invalid Login Attend error message.

Now, if the user wants to confirm his email, then he needs to click on the Confirm Email button on the Login Page as shown in the below image:

How do you block login if email is not confirmed in ASP.NET Core Identity?

Once you click on the Confirm Email button, it will open the following page, where the user needs to enter his email address and then click on the Send Confirmation Email button, as shown in the below image.

How do you block login if email is not confirmed in ASP.NET Core Identity?

Once the user clicks on the Send Confirmation Email button, it will send the confirmation link to the user, and on clicking that click, the user’s email address will be confirmed. We have already discussed this in our previous article.

What about External Login?

We also want to block the login if an external login account (like Facebook, Google, Microsoft, Twitter, etc.) is used and the email address associated with that external account is not confirmed in our database. So, modify the ExternalLoginCallback action method as follows:

[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string? returnUrl, string? remoteError)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    LoginViewModel loginViewModel = new LoginViewModel
    {
        ReturnUrl = returnUrl,
        ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList()
    };

    if (remoteError != null)
    {
        ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");

        return View("Login", loginViewModel);
    }

    // Get the login information about the user from the external login provider
    var info = await signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ModelState.AddModelError(string.Empty, "Error loading external login information.");

        return View("Login", loginViewModel);
    }

    // Email Confirmation Section
    // Get the email claim from external login provider (Google, Facebook etc)
    var email = info.Principal.FindFirstValue(ClaimTypes.Email);
    ApplicationUser? user;

    if (email != null)
    {
        // Find the user
        user = await userManager.FindByEmailAsync(email);

        // If the user exists in our database and email is not confirmed,
        // display login view with validation error
        if (user != null && !user.EmailConfirmed)
        {
            ModelState.AddModelError(string.Empty, "Email not confirmed yet");
            return View("Login", loginViewModel);
        }
    }

    // If the user already has a login (i.e., if there is a record in AspNetUserLogins table)
    // then sign-in the user with this external login provider
    var signInResult = await signInManager.ExternalLoginSignInAsync(info.LoginProvider,
        info.ProviderKey, isPersistent: false, bypassTwoFactor: true);

    if (signInResult.Succeeded)
    {
        return LocalRedirect(returnUrl);
    }

    // If there is no record in AspNetUserLogins table, the user may not have a local account
    else
    {
        if (email != null)
        {
            // Create a new user without password if we do not have a user already
            user = await userManager.FindByEmailAsync(email);

            if (user == null)
            {
                user = new ApplicationUser
                {
                    UserName = info.Principal.FindFirstValue(ClaimTypes.Email),
                    Email = info.Principal.FindFirstValue(ClaimTypes.Email),
                    FirstName = info.Principal.FindFirstValue(ClaimTypes.GivenName),
                    LastName = info.Principal.FindFirstValue(ClaimTypes.Surname),
                };

                //This will create a new user into the AspNetUsers table without password
                await userManager.CreateAsync(user);
            }

            // Add a login (i.e., insert a row for the user in AspNetUserLogins table)
            await userManager.AddLoginAsync(user, info);

            //Then send the Confirmation Email to the User
            await SendConfirmationEmail(email, user);

            //Redirect the user to the Successful Registration Page
            return View("RegistrationSuccessful");
        }

        // If we cannot find the user email we cannot continue
        ViewBag.ErrorTitle = $"Email claim not received from: {info.LoginProvider}";
        ViewBag.ErrorMessage = "Please contact support on info@dotnettutorials.net";

        return View("Error");
    }
}

Note: With the external login, the user does not provide a username and password to our application. Upon successful authentication, the user is redirected to our application’s ExternalLoginCallback() action. So, we know the user is already authenticated, so we display the validation error – Email not confirmed, without checking if the provided username and password combination is correct.

In the next article, I will discuss Forgot Password in ASP.NET Core Identity. In this article, I explain How to Block Login if Email is not Confirmed in ASP.NET Core Identity. I hope you enjoy this article, Block Login if Email is not Confirmed in ASP.NET Core Identity.

1 thought on “Block Login If Email Not Confirmed in ASP.NET Core Identity”

  1. blank
    Diyorbek Eshboboyev

    I have a bug with external login providers.

    “””
    // Email Confirmation Section
    // Get the email claim from external login provider (Google, Facebook etc)
    var email = info.Principal.FindFirstValue(ClaimTypes.Email);
    ApplicationUser? user;

    if (email != null)
    {
    // Find the user
    user = await _userManager.FindByEmailAsync(email);

    // If the user exists in our database and email is not confirmed,
    // display login view with validation error
    if (user != null && !user.EmailConfirmed)
    {
    ModelState.AddModelError(string.Empty, “Email not confirmed yet”);
    return View(“Login”, loginViewModel);
    }
    }
    “””
    This code located in ExternalLoginCallback() action;

    “””
    [HttpGet]
    [AllowAnonymous]
    public async Task ConfirmEmail(string UserId, string Token)
    {
    if (UserId == null || Token == null)
    {
    ViewBag.Message = “The link is Invalid or Expired”;
    }

    // find user
    var user = await _userManager.FindByIdAsync(UserId);
    if (user == null)
    {
    ViewBag.Message = $”The User ID {UserId} is Invalid”;
    return View(“NotFound”);
    }

    // Call the ConfirmEmailAsync method which will mark the Email is Confirmed
    var result = await _userManager.ConfirmEmailAsync(user, Token);
    if (result.Succeeded)
    {
    ViewBag.Message = “Thank you for confirming your email”;
    return View();
    }

    ViewBag.Message = “Email cannot be confirmed”;
    return View();
    }
    “””
    In the arguments to this action, the token comes with the value “null”.

    As a result, the email was not confirmed.

    But in other cases it works successfully.

Leave a Reply

Your email address will not be published. Required fields are marked *