Crear una aplicación web de ASP.NET MVC 5 segura con inicio de sesión, confirmación por correo electrónico y restablecimiento de contraseña (C#)

por Rick Anderson

En este tutorial se muestra cómo crear una aplicación web ASP.NET MVC 5 con confirmación de correo electrónico y restablecimiento de contraseña mediante el sistema de pertenencia de ASP.NET Identity.

Para obtener una versión actualizada de este tutorial en la que se usa .NET Core, vea Confirmación de la cuenta y recuperación de contraseñas en ASP.NET Core.

Creación de una aplicación ASP.NET MVC

Empiece por instalar y ejecutar Visual Studio Express 2013 para Web o Visual Studio 2013. Instale Visual Studio 2013 Update 3 o posterior.

Nota:

Advertencia: Debe instalar Visual Studio 2013 Update 3 o posterior para completar este tutorial.

  1. Cree un proyecto web de ASP.NET y seleccione la plantilla MVC. Web Forms también admite ASP.NET Identity, por lo que puede seguir pasos similares en una aplicación de formularios web.
    Screenshot that shows the New A S P dot Net Project page. The M V C template is selected and Individual User Accounts is highlighted.

  2. Deje la autenticación predeterminada como Cuentas de usuario individuales. Si quiere hospedar la aplicación en Azure, deje activada la casilla. Más adelante en el tutorial se implementará en Azure. Puede abrir una cuenta de Azure de forma gratuita.

  3. Establezca el proyecto para usar SSL.

  4. Ejecute la aplicación, haga clic en el vínculo Registrar y registre un usuario. En este momento, la única validación del correo electrónico es con el atributo [EmailAddress].

  5. En el Explorador de servidores, vaya a Data Connections\DefaultConnection\Tables\AspNetUsers, haga clic con el botón derecho y seleccione Abrir definición de tabla.

    En la siguiente imagen se muestra el esquema AspNetUsers:

    Screenshot that shows the A S P Net Users Script File tab in Server Explorer.

  6. Haga clic con el botón derecho en la tabla AspNetUsers y seleccione Mostrar datos de tabla.
    Screenshot that shows the A S P Net Users schema. The Email Confirmed column labeled as False is highlighted.
    En este momento no se ha confirmado el correo electrónico.

  7. Haga clic en la fila y seleccione Elimina. Agregará este correo electrónico de nuevo en el paso siguiente y enviará un correo electrónico de confirmación.

Confirmación de correo electrónico

Se recomienda confirmar el correo electrónico durante el registro de un nuevo usuario para verificar que no suplanta a otra persona (es decir, que no se haya registrado con el correo electrónico de otra persona). Imagine que tiene un foro de debate, querría evitar que "bob@example.com" se registrar como "joe@contoso.com". Sin confirmación por correo electrónico, "joe@contoso.com" podría recibir correos electrónicos no deseados de la aplicación. Imagine que Bob se ha registrado accidentalmente como "bib@example.com" y no se ha dado cuenta; no podría utilizar la recuperación de contraseña porque la aplicación no tiene su correo electrónico correcto. La confirmación de correo electrónico solo proporciona protección limitada contra bots y no contra spammer determinados, ya que tienen muchos alias de correo electrónico operativos que pueden usar para registrarse.

Por lo general, querrá impedir que los nuevos usuarios publiquen datos en el sitio web antes de haber sido confirmados por correo electrónico,un mensaje de texto SMS u otro mecanismo. En las siguientes secciones, se habilitará la confirmación por correo electrónico y se modificará el código para evitar que los usuarios recién registrados inicien sesión hasta que se haya confirmado su correo electrónico.

Enlace de SendGrid

Las instrucciones de esta sección no están actualizadas. Vea Configuración del proveedor de correo electrónico de SendGrid para obtener instrucciones actualizadas.

Aunque en este tutorial solo se muestra cómo agregar notificaciones por correo electrónico mediante SendGrid, puede enviar correo electrónico mediante SMTP y otros mecanismos (vea recursos adicionales).

  1. En la Consola del Administrador de paquetes, escriba el siguiente comando:

    Install-Package SendGrid
    
  2. Vaya a la página de registro de Azure SendGrid y regístrese para obtener una cuenta gratuita de SendGrid. Para configurar SendGrid, agregue código similar al siguiente en App_Start/IdentityConfig.cs:

    public class EmailService : IIdentityMessageService
    {
       public async Task SendAsync(IdentityMessage message)
       {
          await configSendGridasync(message);
       }
    
       // Use NuGet to install SendGrid (Basic C# client lib) 
       private async Task configSendGridasync(IdentityMessage message)
       {
          var myMessage = new SendGridMessage();
          myMessage.AddTo(message.Destination);
          myMessage.From = new System.Net.Mail.MailAddress(
                              "Joe@contoso.com", "Joe S.");
          myMessage.Subject = message.Subject;
          myMessage.Text = message.Body;
          myMessage.Html = message.Body;
    
          var credentials = new NetworkCredential(
                     ConfigurationManager.AppSettings["mailAccount"],
                     ConfigurationManager.AppSettings["mailPassword"]
                     );
    
          // Create a Web transport for sending email.
          var transportWeb = new Web(credentials);
    
          // Send the email.
          if (transportWeb != null)
          {
             await transportWeb.DeliverAsync(myMessage);
          }
          else
          {
             Trace.TraceError("Failed to create Web transport.");
             await Task.FromResult(0);
          }
       }
    }
    

Tendrá que agregar lo siguiente:

using SendGrid;
using System.Net;
using System.Configuration;
using System.Diagnostics;

Para simplificar este ejemplo, se almacenará la configuración de la aplicación en el archivo web.config:

</connectionStrings>
   <appSettings>
      <add key="webpages:Version" value="3.0.0.0" />
      <!-- Markup removed for clarity. -->
      
      <add key="mailAccount" value="xyz" />
      <add key="mailPassword" value="password" />
   </appSettings>
  <system.web>

Advertencia

Seguridad: Nunca almacene datos confidenciales en el código fuente. La cuenta y las credenciales se almacenan en appSetting. En Azure, puede almacenar estos valores de forma segura en la pestaña Configurar de Azure Portal. Vea Procedimientos recomendados para implementar contraseñas y otros datos confidenciales en ASP.NET y Azure.

Habilitación de la confirmación de correo electrónico en el controlador Account

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

            string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
            var callbackUrl = Url.Action("ConfirmEmail", "Account", 
               new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
            await UserManager.SendEmailAsync(user.Id, 
               "Confirm your account", "Please confirm your account by clicking <a href=\"" 
               + callbackUrl + "\">here</a>");

            return RedirectToAction("Index", "Home");
        }
        AddErrors(result);
    }

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

Compruebe que el archivo Views\Account\ConfirmEmail.cshtml tiene una sintaxis de Razor correcta. (Es posible que falte el carácter @ de la primera línea).

@{
    ViewBag.Title = "Confirm Email";
}

<h2>@ViewBag.Title.</h2>
<div>
    <p>
        Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
    </p>
</div>

Ejecute la aplicación y haga clic en el vínculo Registro. Una vez que envíe el formulario de registro, se iniciará sesión.

Screenshot that shows the My A S P dot NET Log In Home page.

Compruebe la cuenta de correo electrónico y haga clic en el vínculo para confirmar el correo electrónico.

Requerimiento de confirmación por correo electrónico antes de iniciar sesión

Actualmente, una vez que un usuario completa el formulario de registro, se inicia sesión. Por lo general, querrá confirmar su correo electrónico antes de que inicien sesión. En la siguiente sección, modificará el código para exigir que a los nuevos usuarios se les confirme un correo electrónico antes de iniciar sesión (autenticarse). Actualice el método HttpPost Register con los siguientes cambios resaltados:

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
   if (ModelState.IsValid)
   {
      var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
      var result = await UserManager.CreateAsync(user, model.Password);
      if (result.Succeeded)
      {
         //  Comment the following line to prevent log in until the user is confirmed.
         //  await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

         string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
         var callbackUrl = Url.Action("ConfirmEmail", "Account",
            new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
         await UserManager.SendEmailAsync(user.Id, "Confirm your account",
            "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

         // Uncomment to debug locally 
         // TempData["ViewBagLink"] = callbackUrl;

         ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
                         + "before you can log in.";

         return View("Info");
         //return RedirectToAction("Index", "Home");
      }
      AddErrors(result);
   }

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

Al convertir en comentario el método SignInAsync, el usuario no iniciará sesión con el registro. La línea TempData["ViewBagLink"] = callbackUrl; se puede usar para depurar la aplicación y probar el registro sin enviar el correo electrónico. ViewBag.Message se usa para mostrar las instrucciones de confirmación. El ejemplo de descarga contiene código para probar la confirmación del correo electrónico sin configurar el correo electrónico y también se puede usar para depurar la aplicación.

Cree un archivo Views\Shared\Info.cshtml y agregue el siguiente marcado de Razor:

@{
   ViewBag.Title = "Info";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>

Agregue el atributo Authorize al método de acción Contact del controlador Home. Puede hacer clic en el vínculo Contacto para comprobar que los usuarios anónimos no tienen acceso y los usuarios autenticados sí.

[Authorize]
public ActionResult Contact()
{
   ViewBag.Message = "Your contact page.";

   return View();
}

También debe actualizar el método de acción HttpPost Login:

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // Require the user to have a confirmed email before they can log on.
    var user = await UserManager.FindByNameAsync(model.Email);
    if (user != null)
    {
       if (!await UserManager.IsEmailConfirmedAsync(user.Id))
       {
          ViewBag.errorMessage = "You must have a confirmed email to log on.";
          return View("Error");
       }
    }

    // This doesn't count login failures towards account lockout
    // To enable password failures to trigger account lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

Actualice la vista Views\Shared\Error.cshtml para mostrar el mensaje de error:

@model System.Web.Mvc.HandleErrorInfo

@{
    ViewBag.Title = "Error";
}

<h1 class="text-danger">Error.</h1>
@{
   if (String.IsNullOrEmpty(ViewBag.errorMessage))
   {
      <h2 class="text-danger">An error occurred while processing your request.</h2>
   }
   else
   {
      <h2 class="text-danger">@ViewBag.errorMessage</h2>
   }
}

Elimine las cuentas de la tabla AspNetUsers que contengan el alias de correo electrónico que quiera probar. Ejecute la aplicación y compruebe que no puede iniciar sesión hasta que haya confirmado la dirección de correo electrónico. Una vez que confirme la dirección de correo electrónico, haga clic en el vínculo Contacto.

Recuperación y restablecimiento de contraseñas

Quite los caracteres de comentario del método de acción HttpPost ForgotPassword en el controlador de cuenta:

//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindByNameAsync(model.Email);
        if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
        {
            // Don't reveal that the user does not exist or is not confirmed
            return View("ForgotPasswordConfirmation");
        }

        string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
        await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");
        return RedirectToAction("ForgotPasswordConfirmation", "Account");
    }

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

Quite los caracteres de comentario de ForgotPasswordActionLink en el archivo de vista de Razor Views\Account\Login.cshtml:

@using MvcPWy.Models
@model LoginViewModel
@{
   ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
   <div class="col-md-8">
      <section id="loginForm">
         @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
         {
            @Html.AntiForgeryToken()
            <h4>Use a local account to log in.</h4>
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            <div class="form-group">
               @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
               <div class="col-md-10">
                  @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
                  @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
               </div>
            </div>
            <div class="form-group">
               @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
               <div class="col-md-10">
                  @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                  @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <div class="checkbox">
                     @Html.CheckBoxFor(m => m.RememberMe)
                     @Html.LabelFor(m => m.RememberMe)
                  </div>
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <input type="submit" value="Log in" class="btn btn-default" />
               </div>
            </div>
            <p>
               @Html.ActionLink("Register as a new user", "Register")
            </p>
            @* Enable this once you have account confirmation enabled for password reset functionality *@
            <p>
               @Html.ActionLink("Forgot your password?", "ForgotPassword")
            </p>
         }
      </section>
   </div>
   <div class="col-md-4">
      <section id="socialLoginForm">
         @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl })
      </section>
   </div>
</div>

@section Scripts {
   @Scripts.Render("~/bundles/jqueryval")
}

La página Iniciar sesión mostrará ahora un vínculo para restablecer la contraseña.

Una vez que un usuario crea una cuenta local, se le envía por correo electrónico un vínculo de confirmación que debe usar para poder iniciar sesión. Si el usuario elimina accidentalmente el correo electrónico de confirmación o el correo electrónico nunca llega, necesitará que se le envíe de nuevo el vínculo de confirmación. Los siguientes cambios de código muestran cómo habilitar esto.

Agregue el siguiente método auxiliar a la parte superior del archivo Controllers\AccountController.cs:

private async Task<string> SendEmailConfirmationTokenAsync(string userID, string subject)
{
   string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
   var callbackUrl = Url.Action("ConfirmEmail", "Account",
      new { userId = userID, code = code }, protocol: Request.Url.Scheme);
   await UserManager.SendEmailAsync(userID, subject,
      "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

   return callbackUrl;
}

Actualice el método Register para usar el nuevo asistente:

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
   if (ModelState.IsValid)
   {
      var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
      var result = await UserManager.CreateAsync(user, model.Password);
      if (result.Succeeded)
      {
         //  Comment the following line to prevent log in until the user is confirmed.
         //  await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

         string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");

         ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
                         + "before you can log in.";

         return View("Info");
         //return RedirectToAction("Index", "Home");
      }
      AddErrors(result);
   }

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

Actualice el método Login para volver a enviar la contraseña si no se ha confirmado la cuenta de usuario:

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
   if (!ModelState.IsValid)
   {
      return View(model);
   }

   // Require the user to have a confirmed email before they can log on.
  // var user = await UserManager.FindByNameAsync(model.Email);
   var user =  UserManager.Find(model.Email, model.Password);
   if (user != null)
   {
      if (!await UserManager.IsEmailConfirmedAsync(user.Id))
      {
         string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account-Resend");

          // Uncomment to debug locally  
          // ViewBag.Link = callbackUrl;
         ViewBag.errorMessage = "You must have a confirmed email to log on. "
                              + "The confirmation token has been resent to your email account.";
         return View("Error");
      }
   }

   // This doesn't count login failures towards account lockout
   // To enable password failures to trigger account lockout, change to shouldLockout: true
   var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
   switch (result)
   {
      case SignInStatus.Success:
         return RedirectToLocal(returnUrl);
      case SignInStatus.LockedOut:
         return View("Lockout");
      case SignInStatus.RequiresVerification:
         return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
      case SignInStatus.Failure:
      default:
         ModelState.AddModelError("", "Invalid login attempt.");
         return View(model);
   }
}

Combinar cuentas de inicio de sesión locales y sociales

Puede combinar cuentas locales y sociales haciendo clic en el vínculo de correo electrónico. En la siguiente secuencia, RickAndMSFT@gmail.com se crea primero como inicio de sesión local; pero puede crear la cuenta como inicio de sesión social primero y, después, agregar un inicio de sesión local.

Screenshot that shows the My A S P dot Net Log In Home page. The sample User I D is highlighted.

Haga clic en el vínculo Administrar. Observe el valor Inicios de sesión externos: 0 asociado a esta cuenta.

Screenshot that shows the My A S P dot Net Manage your account page. Next to the External Logins line, 0 and a Manage link is highlighted.

Haga clic en el vínculo a otro servicio de inicio de sesión y acepte las solicitudes de la aplicación. Las dos cuentas se han combinado, podrá iniciar sesión con cualquiera de ellas. Es posible que quiera que los usuarios agreguen cuentas locales en caso de que su servicio de autenticación de inicio de sesión social esté inactivo o que hayan perdido el acceso a su cuenta social.

En la siguiente imagen, Tom es un inicio de sesión social (que puede ver en Inicios de sesión externos: 1 que se muestra en la página).

Screenshot that shows the My A S P dot Net Manage your account page. The Pick a password and External Logins lines are highlighted.

Al hacer clic en Elegir una contraseña, puede agregar un inicio de sesión local asociado a la misma cuenta.

Screenshot that shows the My A S P dot Net Create Local Login page. A sample password is entered in the New password and Confirm new password text fields.

Confirmación por correo electrónico más detallada

En el tutorial Confirmación de cuenta y recuperación de contraseñas con ASP.NET Identity se profundiza en este tema con más detalle.

Depuración de la aplicación

Si no recibe un correo electrónico que contenga el vínculo:

Para probar el vínculo de verificación sin correo electrónico, descargue el ejemplo completado. El vínculo de confirmación y los códigos de confirmación se mostrarán en la página.

Recursos adicionales