Confirmación de cuenta y recuperación de contraseña con ASP.NET Identity (C#)

Antes de realizar este tutorial, primero debería completar 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. Este tutorial contiene más detalles y le mostrará cómo configurar el correo electrónico para la confirmación de la cuenta local y permitir que los usuarios restablezcan su contraseña olvidada en ASP.NET Identity.

Una cuenta de usuario local requiere que el usuario cree una contraseña para la cuenta y esa contraseña se almacene (de forma segura) en la aplicación web. ASP.NET Identity también admite cuentas sociales, que no requieren que el usuario cree una contraseña para la aplicación. Las cuentas sociales usan un tercero (como Google, Twitter, Facebook o Microsoft) para autenticar a los usuarios. En este tema se trata lo siguiente:

Los nuevos usuarios registran su alias de correo electrónico, que crea una cuenta local.

Image of the account register window

Al seleccionar el botón Registrar, se envía un correo electrónico de confirmación que contiene un token de validación a su dirección de correo electrónico.

Image showing email sent confirmation

Se envía al usuario un correo electrónico con un token de confirmación para su cuenta.

Image of confirmation token

Al seleccionar el vínculo se confirma la cuenta.

Image confirming email address

Recuperación y restablecimiento de contraseñas

Los usuarios locales que olvidan su contraseña pueden recibir un token de seguridad en su cuenta de correo electrónico, lo que les permite restablecer su contraseña.

Image of forgot password reset window

El usuario pronto recibirá un correo electrónico con un vínculo que les permitirá restablecer su contraseña.

Image showing reset password email
Al seleccionar el vínculo llegarán a la página de restablecimiento.

Image showing user password reset window

Al seleccionar el botón Restablecer se confirmará que se ha restablecido la contraseña.

Image showing password reset confirmation

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

Empiece por instalar y ejecutar Visual Studio 2017.

  1. Cree un nuevo proyecto web de ASP.NET y seleccione la plantilla MVC. Web Forms también admite ASP.NET Identity, por lo que podría seguir pasos similares en una aplicación de formularios web.

  2. Cambie la autenticación a Cuentas de usuario individual.

  3. Ejecute la aplicación, seleccione el vínculo Registro y registre un usuario. En este momento, la única validación del correo electrónico es con el atributo [EmailAddress].

  4. 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:

    Image showing A s p Net Users schema

  5. Haga clic con el botón derecho en la tabla AspNetUsers y seleccione Mostrar datos de tabla.

    Image showing table data

    En este momento no se ha confirmado el correo electrónico.

El almacén de datos predeterminado para ASP.NET Identity es Entity Framework, pero puede configurarlo para usar otros almacenes de datos y agregar campos adicionales. Consulte la sección Recursos adicionales al final de este tutorial.

Se llama a la clase de inicio OWIN (Startup.cs) cuando se inicia la aplicación e invoca el método ConfigureAuth en App_Start\Startup.Auth.cs, que configura la canalización de OWIN e inicializa ASP.NET Identity. Examine el método ConfigureAuth. Cada llamada CreatePerOwinContext registra una devolución de llamada (guardada en OwinContext) a la que se llamará una vez por solicitud para crear una instancia del tipo especificado. Puede establecer un punto de interrupción en el constructor y el método Create de cada tipo (ApplicationDbContext, ApplicationUserManager), y comprobar que se llaman en cada solicitud. Una instancia de ApplicationDbContext y ApplicationUserManager se almacena en el contexto de OWIN, al que se puede acceder en toda la aplicación. ASP.NET Identity se enlaza a la canalización de OWIN a través del middleware de cookies. Para obtener más información, consulte Administración por duración de solicitudes para la clase UserManager en ASP.NET Identity.

Al cambiar el perfil de seguridad, se genera y almacena una nueva marca de seguridad en el campo SecurityStamp de la tabla AspNetUsers. Tenga en cuenta que el campo SecurityStamp es diferente de la cookie de seguridad. La cookie de seguridad no se almacena en la tabla AspNetUsers (o en ningún otro lugar de la base de datos de identidad). El token de cookie de seguridad se autofirma mediante DPAPI y se crea con UserId, SecurityStamp y la información de tiempo de expiración.

El middleware de cookies comprueba la cookie en cada solicitud. El método SecurityStampValidator de la clase Startup alcanza la base de datos y comprueba periódicamente la marca de seguridad, tal como se especifica con validateInterval. Esto solo ocurre cada 30 minutos (en nuestro ejemplo) a menos que cambie su perfil de seguridad. Se eligió el intervalo de 30 minutos para minimizar los viajes a la base de datos. Consulte el tutorial de autenticación en dos fases para obtener más información.

Según los comentarios del código, el método UseCookieAuthentication admite la autenticación de cookies. El campo SecurityStamp y el código asociado proporcionan una capa adicional de seguridad a la aplicación, al cambiar la contraseña se cerrará la sesión del explorador con el que inició sesión. El método SecurityStampValidator.OnValidateIdentity habilita a la aplicación validar el token de seguridad cuando el usuario inicia sesión, que se usa al cambiar una contraseña o usar el inicio de sesión externo. Esto es necesario para asegurar que se invalida cualquier token generado con la contraseña antigua. En el proyecto de ejemplo, si cambia la contraseña del usuario se genera un nuevo token para el usuario, y se invalidan el token anteriores y se actualiza el campo SecurityStamp.

El sistema de Identity le permite configurar la aplicación para que cuando cambie el perfil de seguridad de los usuarios (por ejemplo, cuando el usuario cambia su contraseña o cambia el inicio de sesión asociado (por ejemplo, desde Facebook, Google, cuenta de Microsoft, etc.), se cierra la sesión de usuario de todas las instancias del explorador. Por ejemplo, la imagen siguiente muestra la aplicación de cierre de sesión único de ejemplo, que permite al usuario cerrar sesión en todas las instancias del explorador (en este caso, IE, Firefox y Chrome) seleccionando un botón. Como alternativa, el ejemplo solo permite cerrar sesión en una instancia específica del explorador.

Image showing the single sign-out sample app window

La aplicación de cierre de sesión único de ejemplo muestra cómo ASP.NET Identity le permite volver a generar el token de seguridad. Esto es necesario para asegurar que se invalida cualquier token (cookies) generado con la contraseña antigua. Esta característica proporciona una capa adicional de seguridad a su aplicación, ya que cuando cambie su contraseña, se cerrará su sesión donde haya iniciado sesión en esta aplicación.

El archivo App_Start\IdentityConfig.cs contiene las clases ApplicationUserManager, EmailService y SmsService. Las clases EmailService y SmsService implementan la interfaz IIdentityMessageService, por lo que tiene métodos comunes en cada clase para configurar correo electrónico y SMS. Aunque en este tutorial solo se muestra cómo agregar notificaciones por correo electrónico a través de SendGrid, puede enviar correo electrónico mediante SMTP y otros mecanismos.

La clase Startup también contiene texto reutilizable para agregar inicios de sesión sociales (Facebook, Twitter, etc.), consulte mi tutorial Inicio de sesión en aplicación MVC 5 con Facebook, Twitter, LinkedIn y Google OAuth2 para obtener más información.

Examine la clase ApplicationUserManager, que contiene la información de identidad de los usuarios y configura las siguientes características:

  • Requisitos de fuerza de contraseña.
  • Bloqueo del usuario (intentos y tiempo).
  • Autenticación en dos fases (2FA). Trataré 2FA y SMS en otro tutorial.
  • Enlace de los servicios de correo electrónico y SMS. (Voy a cubrir SMS en otro tutorial).

La clase ApplicationUserManager deriva de la clase genérica UserManager<ApplicationUser>. ApplicationUser deriva de IdentityUser. IdentityUser deriva de la clase genérica IdentityUser:

//     Default EntityFramework IUser implementation
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
   where TLogin : IdentityUserLogin<TKey>
   where TRole : IdentityUserRole<TKey>
   where TClaim : IdentityUserClaim<TKey>
{
   public IdentityUser()
   {
      Claims = new List<TClaim>();
      Roles = new List<TRole>();
      Logins = new List<TLogin>();
   }

   ///     User ID (Primary Key)
   public virtual TKey Id { get; set; }

   public virtual string Email { get; set; }
   public virtual bool EmailConfirmed { get; set; }

   public virtual string PasswordHash { get; set; }

   ///     A random value that should change whenever a users credentials have changed (password changed, login removed)
   public virtual string SecurityStamp { get; set; }

   public virtual string PhoneNumber { get; set; }
   public virtual bool PhoneNumberConfirmed { get; set; }

   public virtual bool TwoFactorEnabled { get; set; }

   ///     DateTime in UTC when lockout ends, any time in the past is considered not locked out.
   public virtual DateTime? LockoutEndDateUtc { get; set; }

   public virtual bool LockoutEnabled { get; set; }

   ///     Used to record failures for the purposes of lockout
   public virtual int AccessFailedCount { get; set; }
   
   ///     Navigation property for user roles
   public virtual ICollection<TRole> Roles { get; private set; }

   ///     Navigation property for user claims
   public virtual ICollection<TClaim> Claims { get; private set; }

   ///     Navigation property for user logins
   public virtual ICollection<TLogin> Logins { get; private set; }
   
   public virtual string UserName { get; set; }
}

Las propiedades anteriores coinciden con las propiedades de la tabla AspNetUsers, mostradas anteriormente.

Los argumentos genéricos en IUser le permiten derivar una clase mediante tipos diferentes para la clave principal. Consulte el ejemplo ChangePK que muestra cómo cambiar la clave principal de la cadena a int o GUID.

ApplicationUser

ApplicationUser (public class ApplicationUserManager : UserManager<ApplicationUser>) se define en Models\IdentityModels.cs como:

public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
        UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in 
       //   CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, 
    DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }
}

El código resaltado anterior genera una ClaimsIdentity. ASP.NET Identity y la autenticación de cookies OWIN se basan en notificaciones, por lo que el marco requiere que la aplicación genere un ClaimsIdentity para el usuario. ClaimsIdentity tiene información sobre todas las notificaciones del usuario, como el nombre del usuario, la edad y los roles a los que pertenece el usuario. También puede agregar más notificaciones para el usuario en esta fase.

El método OWIN AuthenticationManager.SignIn pasa en ClaimsIdentity e inicia sesión en el usuario:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties(){
       IsPersistent = isPersistent }, 
       await user.GenerateUserIdentityAsync(UserManager));
}

Inicio de sesión en aplicación MVC 5 con Facebook, Twitter, LinkedIn y Google OAuth2 muestra cómo puede agregar propiedades adicionales a la clase ApplicationUser.

Confirmación de correo electrónico

Es una buena idea confirmar el correo electrónico con el que se registra un nuevo usuario para comprobar que no suplanta a otra persona (es decir, no se han registrado con el correo electrónico de otra persona). Supongamos que tiene un foro de debate y quiere evitar que "bob@example.com" se registre 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 proporciona protección contra determinados spammers, que tienen muchos alias de correo electrónico de trabajo que pueden usar para registrarse. En el ejemplo siguiente, el usuario no podrá cambiar su contraseña hasta que se haya confirmado su cuenta (seleccionando un vínculo de confirmación recibido en la cuenta de correo electrónico con la que se registró). Puede aplicar este flujo de trabajo a otros escenarios, por ejemplo, enviando un vínculo para confirmar y restablecer la contraseña en las cuentas nuevas creadas por el administrador, enviando al usuario un correo electrónico cuando haya cambiado su perfil, etc. Por lo general, querrá impedir que los nuevos usuarios publiquen datos en el sitio web antes de haber sido confirmados por correo electrónico, mensaje de texto SMS u otro mecanismo.

Compilar un ejemplo más completo

En esta sección usará NuGet para descargar un ejemplo con el que trabajaremos.

  1. Cree un nuevo proyecto web de ASP.NET vacío.

  2. En la consola del administrador de paquetes escriba los siguientes comandos:

    Install-Package SendGrid
    Install-Package -Prerelease Microsoft.AspNet.Identity.Samples
    

    En este tutorial usaremos SendGrid para enviar correo electrónico. El paquete Identity.Samples instala el código con el que trabajaremos.

  3. Establezca el proyecto para usar SSL.

  4. Pruebe la creación de la cuenta local ejecutando la aplicación, seleccionando el vínculo Registro y publicando el formulario de registro.

  5. Seleccione el vínculo de correo electrónico de demostración, que simula la confirmación del correo electrónico.

  6. Quite el código de confirmación del vínculo de correo electrónico de demostración del ejemplo (el código ViewBag.Link del controlador de cuenta. Consulte los métodos de acción DisplayEmail y ForgotPasswordConfirmation y las vistas Razor).

Advertencia

Si cambia cualquiera de las opciones de seguridad de este ejemplo, las aplicaciones de producción deberán someterse a una auditoría de seguridad que llame explícitamente a los cambios realizados.

Examinar el código en App_Start\IdentityConfig.cs

En el ejemplo se muestra cómo crear una cuenta y agregarla al rol Administrador. Debería reemplazar el correo electrónico del ejemplo por el correo electrónico que usará para la cuenta de administrador. La manera más fácil ahora de crear una cuenta de administrador es mediante programación en el método Seed. Esperamos tener una herramienta en el futuro que le permita crear y administrar usuarios y roles. El código de ejemplo le permite crear y administrar usuarios y roles, pero primero debe tener una cuenta de administrador para ejecutar los roles y las páginas de administrador de usuario. En este ejemplo, la cuenta de administrador se crea cuando se propague la base de datos.

Cambie la contraseña y cambie el nombre a una cuenta donde puede recibir notificaciones por correo electrónico.

Advertencia

Seguridad: nunca almacene datos confidenciales en el código fuente.

Como se mencionó anteriormente, la llamada app.CreatePerOwinContext en la clase de inicio agrega devoluciones de llamada al método Create del contenido de la base de datos de la aplicación, y las clases de administrador de usuarios y de administrador de roles. La canalización OWIN llama al método Create en estas clases para cada solicitud y almacena el contexto de cada clase. El controlador de cuenta expone el administrador de usuarios desde el contexto HTTP (que contiene el contexto OWIN):

public ApplicationUserManager UserManager
{
    get
    {
        return _userManager ?? 
    HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    }
    private set
    {
        _userManager = value;
    }
}

Cuando un usuario registra una cuenta local, se llama al método HTTP Post 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)
        {
            var 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 this link: <a href=\"" 
                                               + callbackUrl + "\">link</a>");
            // ViewBag.Link = callbackUrl;   // Used only for initial demo.
            return View("DisplayEmail");
        }
        AddErrors(result);
    }

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

El código anterior usa los datos del modelo para crear una nueva cuenta de usuario mediante el correo electrónico y la contraseña introducidos. Si el alias de correo electrónico está en el almacén de datos se produce un error en la creación de la cuenta y se vuelve a mostrar el formulario. El método GenerateEmailConfirmationTokenAsync crea un token de confirmación seguro y lo almacena en el almacén de datos de ASP.NET Identity. El método Url.Action crea un vínculo que contiene el UserId y el token de confirmación. A continuación, este vínculo se envía por correo electrónico al usuario; el usuario puede seleccionarlo en el vínculo de su aplicación de correo electrónico para confirmar su cuenta.

Configurar la confirmación por correo electrónico

Vaya a la página de registro de SendGrid y regístrese para obtener una cuenta gratuita. Agregue código similar al siguiente para configurar SendGrid:

public class EmailService : IIdentityMessageService
{
   public Task SendAsync(IdentityMessage message)
   {
      return configSendGridasync(message);
   }

   private 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)
      {
         return transportWeb.DeliverAsync(myMessage);
      }
      else
      {
         return Task.FromResult(0);
      }
   }
}

Nota:

Los clientes de correo electrónico suelen aceptar solo mensajes de texto (sin HTML). Debería proporcionar el mensaje en texto y HTML. En el ejemplo de SendGrid anterior, esto se hace con el código myMessage.Text y myMessage.Html mostrado anteriormente.

En el código siguiente se muestra cómo enviar correo electrónico mediante la clase MailMessage, donde message.Body solo devuelve el vínculo.

void sendMail(Message message)
{
#region formatter
   string text = string.Format("Please click on this link to {0}: {1}", message.Subject, message.Body);
   string html = "Please confirm your account by clicking this link: <a href=\"" + message.Body + "\">link</a><br/>";

   html += HttpUtility.HtmlEncode(@"Or click on the copy the following link on the browser:" + message.Body);
#endregion

   MailMessage msg = new MailMessage();
   msg.From = new MailAddress("joe@contoso.com");
   msg.To.Add(new MailAddress(message.Destination));
   msg.Subject = message.Subject;
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));

   SmtpClient smtpClient = new SmtpClient("smtp.gmail.com", Convert.ToInt32(587));
   System.Net.NetworkCredential credentials = new System.Net.NetworkCredential("joe@contoso.com", "XXXXXX");
   smtpClient.Credentials = credentials;
   smtpClient.EnableSsl = true;
   smtpClient.Send(msg);
}

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. Consulte Prácticas recomendadas para implementar contraseñas y otros datos confidenciales en ASP.NET y Azure.

Escriba las credenciales de SendGrid, ejecute la aplicación y regístrese con un alias de correo electrónico; puede seleccionar el vínculo confirmar en el correo electrónico. Para ver cómo hacerlo con su cuenta de correo electrónico de Outlook.com, consulte los post de John Atten Configuración SMTP de C# para host SMPT de Outlook.com y ASP.NET Identity 2.0: configurar validación de cuentas y autorización en dos fases.

Una vez que un usuario selecciona el botón Registro se envía un correo electrónico de confirmación que contiene un token de validación a su dirección de correo electrónico.

Image of email sent confirmation window

Se envía al usuario un correo electrónico con un token de confirmación para su cuenta.

Image of email received

Examen del código

En el código siguiente se muestra el método POST ForgotPassword.

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");
        }

        var 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 here: <a href=\"" + callbackUrl + "\">link</a>");        
        return View("ForgotPasswordConfirmation");
    }

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

El método produce un error en modo silencioso si no se ha confirmado el correo electrónico del usuario. Si se publicó un error para una dirección de correo electrónico no válida, los usuarios malintencionados podrían usar esa información para encontrar userId (alias de correo electrónico) válidos para atacar.

El código siguiente muestra el método ConfirmEmail en el controlador de cuenta al que se llama cuando el usuario selecciona el vínculo de confirmación en el correo electrónico enviado:

public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
    if (userId == null || code == null)
    {
        return View("Error");
    }
    var result = await UserManager.ConfirmEmailAsync(userId, code);
    if (result.Succeeded)
    {
        return View("ConfirmEmail");
    }
    AddErrors(result);
    return View();
}

Una vez que se ha usado un token de contraseña olvidado, se invalida. El siguiente cambio de código en el método Create (en el App_Start\IdentityConfig.cs archivo) establece que los tokens expiren en tres horas.

if (dataProtectionProvider != null)
 {
    manager.UserTokenProvider =
       new DataProtectorTokenProvider<ApplicationUser>
          (dataProtectionProvider.Create("ASP.NET Identity"))
          {                    
             TokenLifespan = TimeSpan.FromHours(3)
          };
 }

Con el código anterior, la contraseña olvidada y los tokens de confirmación de correo electrónico expirarán en tres horas. El valor TokenLifespan predeterminado es un día.

El código siguiente muestra el método de confirmación de correo electrónico:

// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
   if (userId == null || code == null)
   {
      return View("Error");
   }
   IdentityResult result;
   try
   {
      result = await UserManager.ConfirmEmailAsync(userId, code);
   }
   catch (InvalidOperationException ioe)
   {
      // ConfirmEmailAsync throws when the userId is not found.
      ViewBag.errorMessage = ioe.Message;
      return View("Error");
   }

   if (result.Succeeded)
   {
      return View();
   }

   // If we got this far, something failed.
   AddErrors(result);
   ViewBag.errorMessage = "ConfirmEmail failed";
   return View("Error");
}

Para hacer la aplicación más segura, ASP.NET Identity admite la autenticación en dos fases (2FA). Consulte ASP.NET Identity 2.0: configurar la validación de cuentas y la autorización en dos fases de John Atten. Aunque puede establecer el bloqueo de cuenta en los errores de contraseña en los intentos de inicio de sesión, ese enfoque hace que el inicio de sesión sea susceptible a los bloqueos DOS. Se recomienda usar el bloqueo de cuenta solo con 2FA.

Recursos adicionales