Autenticación en dos fases mediante SMS y correo electrónico con ASP.NET Identity

por Hao Kung, Pranav Rastogi, Rick Anderson, Suhas Joshi

En este tutorial se muestra cómo configurar la autenticación en dos fases (2FA) mediante SMS y correo electrónico.

Este artículo fue escrito por Rick Anderson (@RickAndMSFT), Pranav Rastogi (@rustd), Hao Kung y Suhas Joshi. El ejemplo de NuGet fue escrito principalmente por Hao Kung.

En este tema se trata lo siguiente:

Compilación del ejemplo de identidad

En esta sección, usará NuGet para descargar un ejemplo con el que trabajaremos. Empiece por instalar y ejecutar Visual Studio Express 2013 para Web o Visual Studio 2013. Instale Visual Studio 2013 Update 2 o superior.

Nota:

Advertencia: debe instalar Visual Studio 2013 Update 2 para completar este tutorial.

  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 y Twilio o ASPSMS para el texto SMS. El paquete Identity.Samples instala el código con el que trabajaremos.

  3. Establezca el proyecto para usar SSL.

  4. Opcional: siga las instrucciones del tutorial Confirmación de correo electrónico para enlazar SendGrid y, a continuación, ejecute la aplicación y registre una cuenta de correo electrónico.

  5. Opcional: 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. Vea los métodos de acción DisplayEmail y ForgotPasswordConfirmation y las vistas Razor).

  6. Opcional: quite el código ViewBag.Status de los controladores de administración y cuenta y de las vistas de Razor Views\Account\VerifyCode.cshtml y Views\Manage\VerifyPhoneNumber.cshtml. Como alternativa, puede mantener la pantalla ViewBag.Status para probar cómo funciona localmente esta aplicación sin tener que enlazar y enviar mensajes SMS y correo electrónico.

Nota:

Advertencia: si cambia alguna 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.

Configuración de SMS para la autenticación en dos fases

En este tutorial se proporcionan instrucciones para usar Twilio o ASPSMS, pero puede usar cualquier otro proveedor de SMS.

  1. Creación de una cuenta de usuario con un proveedor de SMS

    Cree una cuenta de Twilio o ASPSMS.

  2. Instalación de paquetes adicionales o adición de referencias de servicio

    Twilio:
    En la Consola del Administrador de paquetes, escriba el siguiente comando:
    Install-Package Twilio

    ASPSMS:
    Es necesario agregar la siguiente referencia de servicio:

    Image of add service reference window

    Dirección:
    https://webservice.aspsms.com/aspsmsx2.asmx?WSDL

    Espacio de nombres:
    ASPSMSX2

  3. Averiguar las credenciales de usuario del proveedor de SMS

    Twilio:
    En la pestaña Panel de la cuenta de Twilio, copie el SID de cuenta y el token de autenticación.

    ASPSMS:
    En la configuración de la cuenta, vaya a Clave de usuario y cópiela junto con la contraseña autodefinida.

    Más adelante almacenaremos estos valores en las variables SMSAccountIdentification y SMSAccountPassword.

  4. Especificación de ID del emisor/Originador

    Twilio:
    En la pestaña Números, copie su número de teléfono de Twilio.

    ASPSMS:
    En el menú Desbloquear originadores, desbloquee uno o varios originadores o elija un originador alfanumérico (no es compatible con todas las redes).

    Más adelante almacenaremos este valor en la variable SMSAccountFrom.

  5. Transferencia de credenciales del proveedor de SMS a la aplicación

    Haga que las credenciales y el número de teléfono del remitente estén disponibles para la aplicación:

    public static class Keys
    {
       public static string SMSAccountIdentification = "My Idenfitication";
       public static string SMSAccountPassword = "My Password";
       public static string SMSAccountFrom = "+15555551234";
    }
    

    Advertencia

    Seguridad: nunca almacene datos confidenciales en el código fuente. La cuenta y las credenciales se agregan al código anterior para simplificar el ejemplo. Consulte MVC de ASP.NET: Mantener la configuración privada fuera del control de código fuente por Jon Atten.

  6. Implementación de la transferencia de datos al proveedor de SMS

    Configure la clase SmsService en el archivo App_Start\IdentityConfig.cs.

    En función del proveedor de SMS usado, active la sección Twilio o ASPSMS:

    public class SmsService : IIdentityMessageService
    {
        public Task SendAsync(IdentityMessage message)
        {
            // Twilio Begin
            // var Twilio = new TwilioRestClient(
            //   Keys.SMSAccountIdentification,
            //   Keys.SMSAccountPassword);
            // var result = Twilio.SendMessage(
            //   Keys.SMSAccountFrom,
            //   message.Destination, message.Body
            // );
            // Status is one of Queued, Sending, Sent, Failed or null if the number is not valid
            // Trace.TraceInformation(result.Status);
            // Twilio doesn't currently have an async API, so return success.
            // return Task.FromResult(0);
            // Twilio End
    
            // ASPSMS Begin 
            // var soapSms = new WebApplication1.ASPSMSX2.ASPSMSX2SoapClient("ASPSMSX2Soap");
            // soapSms.SendSimpleTextSMS(
            //   Keys.SMSAccountIdentification,
            //   Keys.SMSAccountPassword,
            //   message.Destination,
            //   Keys.SMSAccountFrom,
            //   message.Body);
            // soapSms.Close();
            // return Task.FromResult(0);
            // ASPSMS End
        }
    }
    
  7. Ejecute la aplicación e inicie sesión con la cuenta que registró anteriormente.

  8. Haga clic en el identificador de usuario, que activa el método de acción Index en el controlador Manage.

    Image of registered account logged into the app

  9. Haga clic en Agregar.

    Image of add phone number link

  10. En unos segundos recibirá un mensaje de texto con el código de verificación. Introdúzcalo y presione Enviar.

    Image showing phone verification code entry

  11. La vista Administrar muestra que se ha agregado el número de teléfono.

    Image of manage view window showing phone number

Examen del código

// GET: /Account/Index
public async Task<ActionResult> Index(ManageMessageId? message)
{
    ViewBag.StatusMessage =
        message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed."
        : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set."
        : message == ManageMessageId.SetTwoFactorSuccess ? "Your two factor provider has been set."
        : message == ManageMessageId.Error ? "An error has occurred."
        : message == ManageMessageId.AddPhoneSuccess ? "The phone number was added."
        : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed."
        : "";

    var model = new IndexViewModel
    {
        HasPassword = HasPassword(),
        PhoneNumber = await UserManager.GetPhoneNumberAsync(User.Identity.GetUserId()),
        TwoFactor = await UserManager.GetTwoFactorEnabledAsync(User.Identity.GetUserId()),
        Logins = await UserManager.GetLoginsAsync(User.Identity.GetUserId()),
        BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(User.Identity.GetUserId())
    };
    return View(model);
}

El método de acción Index del controlador Manage establece el mensaje de estado en función de la acción anterior y proporciona vínculos para cambiar la contraseña local o agregar una cuenta local. El método Index también muestra el estado o el número de teléfono de 2FA, inicios de sesión externos, 2FA habilitados y recordar el método 2FA para este explorador (explicado más adelante). Al hacer clic en el identificador de usuario (correo electrónico) de la barra de título no se pasa un mensaje. Al hacer clic en el vínculo Número de teléfono : quitar vínculo se pasa Message=RemovePhoneSuccess como una cadena de consulta.

https://localhost:44300/Manage?Message=RemovePhoneSuccess

[Image of phone number removed]

El método de acción AddPhoneNumber muestra un cuadro de diálogo para escribir un número de teléfono que puede recibir mensajes SMS.

// GET: /Account/AddPhoneNumber
public ActionResult AddPhoneNumber()
{
   return View();
}

Image of add phone number action dialog box

Al hacer clic en el botón Enviar código de verificación, se envía el número de teléfono al método de acción HTTP POST AddPhoneNumber.

// POST: /Account/AddPhoneNumber
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> AddPhoneNumber(AddPhoneNumberViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // Generate the token 
    var code = await UserManager.GenerateChangePhoneNumberTokenAsync(
                               User.Identity.GetUserId(), model.Number);
    if (UserManager.SmsService != null)
    {
        var message = new IdentityMessage
        {
            Destination = model.Number,
            Body = "Your security code is: " + code
        };
        // Send token
        await UserManager.SmsService.SendAsync(message);
    }
    return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number });
}

El método GenerateChangePhoneNumberTokenAsync genera el token de seguridad que se establecerá en el mensaje SMS. Si se ha configurado el servicio SMS, el token se envía como la cadena "El código de seguridad es <token>". Se llama al método SmsService.SendAsync de forma asincrónica, la aplicación se redirige al método de acción VerifyPhoneNumber (que muestra el siguiente cuadro de diálogo), donde puede escribir el código de verificación.

Image of verify phone number action method dialog box

Una vez que escriba el código y haga clic en enviar, el código se publicará en el método de acción HTTP POST VerifyPhoneNumber.

// POST: /Account/VerifyPhoneNumber
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> VerifyPhoneNumber(VerifyPhoneNumberViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    var result = await UserManager.ChangePhoneNumberAsync(User.Identity.GetUserId(), model.PhoneNumber, model.Code);
    if (result.Succeeded)
    {
        var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
        if (user != null)
        {
            await SignInAsync(user, isPersistent: false);
        }
        return RedirectToAction("Index", new { Message = ManageMessageId.AddPhoneSuccess });
    }
    // If we got this far, something failed, redisplay form
    ModelState.AddModelError("", "Failed to verify phone");
    return View(model);
}

El método ChangePhoneNumberAsync comprueba el código de seguridad publicado. Si el código es correcto, el número de teléfono se agrega al campo PhoneNumber de la tabla AspNetUsers. Si esa llamada se realiza correctamente, se llama al método SignInAsync:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
   // Clear the temporary cookies used for external and two factor sign ins
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, 
       DefaultAuthenticationTypes.TwoFactorCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties
    {
       IsPersistent = isPersistent 
    }, 
       await user.GenerateUserIdentityAsync(UserManager));
}

El parámetro isPersistent establece si la sesión de autenticación se conserva en varias solicitudes.

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.

Se debe llamar al método SignInAsync cuando se realiza cualquier cambio en el perfil de seguridad. Cuando cambia el perfil de seguridad, la base de datos actualiza el campo SecurityStamp y sin llamar al métodoSignInAsync, permanecería conectado en only la próxima vez que la canalización de OWIN alcance la base de datos (la validateInterval). Para probarlo, cambie el método SignInAsync para que se devuelva inmediatamente y establezca la propiedad de cookie validateInterval de 30 minutos a 5 segundos:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
   return;

   // Clear any partial cookies from external or two factor partial sign ins
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, 
       DefaultAuthenticationTypes.TwoFactorCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties
    {
       IsPersistent = isPersistent 
    }, 
       await user.GenerateUserIdentityAsync(UserManager));
}
public void ConfigureAuth(IAppBuilder app) {
    // Configure the db context, user manager and role manager to use a single instance per request
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
    app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
    app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

    // Enable the application to use a cookie to store information for the signed in user
    // and to use a cookie to temporarily store information about a user logging in with a 
    // third party login provider
    // Configure the sign in cookie
    app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider {
            // Enables the application to validate the security stamp when the user logs in.
            // This is a security feature which is used when you change a password or add 
            // an external login to your account.  
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                //validateInterval: TimeSpan.FromMinutes(30),
                validateInterval: TimeSpan.FromSeconds(5),
                regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
        }
    });
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

Con los cambios de código anteriores, puede cambiar el perfil de seguridad (por ejemplo, cambiando el estado de Two Factor Enabled) y se cerrará la sesión en 5 segundos cuando se produzca un error en el método SecurityStampValidator.OnValidateIdentity. Quite la línea de retorno en el método SignInAsync, realice otro cambio en el perfil de seguridad y no se cerrará la sesión. El método SignInAsync genera una nueva cookie de seguridad.

Habilitación de la autenticación en dos fases

En la aplicación de ejemplo, debe usar la interfaz de usuario para habilitar la autenticación en dos fases (2FA). Para habilitar 2FA, haga clic en el identificador de usuario (alias de correo electrónico) en la barra de navegación.Image of U I to enable two-factor authentication
Haga clic en habilitar 2FA.Image after clicking user I D showing two-factor authentication enable link Cierre sesión y vuelva a iniciar sesión. Si ha habilitado el correo electrónico (consulte el tutorial anterior), puede seleccionar el SMS o el correo electrónico para 2FA.Image displaying verification send options La página Comprobar código se muestra donde puede escribir el código (desde SMS o correo electrónico).Image of verify code page Al hacer clic en la casilla Recordar este explorador, se le excluirá de tener que usar 2FA para iniciar sesión con ese equipo y explorador. Habilitar 2FA y hacer clic en Recordar este navegador le proporcionará una protección segura de 2FA frente a usuarios malintencionados que intentan acceder a su cuenta, siempre y cuando no tengan acceso al equipo. Puede hacerlo en cualquier máquina privada que use con regularidad. Al establecer Recordar este explorador, obtiene la seguridad agregada de 2FA desde los equipos que no usa regularmente y obtiene la comodidad de no tener que pasar por 2FA en sus propios equipos.

Registro de un proveedor de autenticación en dos fases

Al crear un nuevo proyecto de MVC, el archivo IdentityConfig.cs contiene el código siguiente para registrar un proveedor de autenticación en dos fases:

public static ApplicationUserManager Create(
   IdentityFactoryOptions<ApplicationUserManager> options, 
   IOwinContext context) 
{
    var manager = new ApplicationUserManager(
       new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };
    // Configure validation logic for passwords
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6,
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };
    // Register two factor authentication providers. This application uses Phone and Emails as a 
    // step of receiving a code for verifying the user
    // You can write your own provider and plug it in here.
    manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser>
    {
        MessageFormat = "Your security code is: {0}"
    });
    manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser>
    {
        Subject = "Security Code",
        BodyFormat = "Your security code is: {0}"
    });
    manager.EmailService = new EmailService();
    manager.SmsService = new SmsService();

    var dataProtectionProvider = options.DataProtectionProvider;
    if (dataProtectionProvider != null)
    {
        manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>
           (dataProtectionProvider.Create("ASP.NET Identity"));
    }
    return manager;
}

Adición de un número de teléfono para 2FA

El método de acción AddPhoneNumber del controlador Manage genera un token de seguridad y lo envía al número de teléfono proporcionado.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> AddPhoneNumber(AddPhoneNumberViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    // Generate the token and send it
    var code = await UserManager.GenerateChangePhoneNumberTokenAsync(
       User.Identity.GetUserId(), model.Number);
    if (UserManager.SmsService != null)
    {
        var message = new IdentityMessage
        {
            Destination = model.Number,
            Body = "Your security code is: " + code
        };
        await UserManager.SmsService.SendAsync(message);
    }
    return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number });
}

Después de enviar el token, se redirige al método de acción VerifyPhoneNumber, donde puede escribir el código para registrar SMS para 2FA. La 2FA con SMS no se usa hasta que haya comprobado el número de teléfono.

Habilitación de 2FA

El método de acción EnableTFA habilita 2FA:

// POST: /Manage/EnableTFA
[HttpPost]
public async Task<ActionResult> EnableTFA()
{
    await UserManager.SetTwoFactorEnabledAsync(User.Identity.GetUserId(), true);
    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user != null)
    {
        await SignInAsync(user, isPersistent: false);
    }
    return RedirectToAction("Index", "Manage");
}

Tenga en cuenta que se debe llamar a SignInAsync porque enable 2FA es un cambio en el perfil de seguridad. Cuando se habilita 2FA, el usuario tendrá que usar 2FA para iniciar sesión, mediante los enfoques de 2FA que han registrado (SMS y correo electrónico en el ejemplo).

Puede agregar más proveedores de 2FA, como generadores de código QR, o puede escribir sus propios.

Nota:

Los códigos 2FA se generan mediante el algoritmo de contraseña de un solo uso basado en tiempo y los códigos son válidos durante seis minutos. Si tarda más de seis minutos en escribir el código, recibirá un mensaje de error de código no válido.

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, a continuación, agregar un inicio de sesión local.

Image selecting email link

Haga clic en el vínculo Administrar. Tenga en cuenta el 0 externo (inicios de sesión sociales) asociados a esta cuenta.

Image displaying next page and selecting manage

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 haya perdido el acceso a su cuenta social.

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

Image showing external logins and location of pick a password

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

Image of pick a password page

Bloqueo de cuentas de ataques por fuerza bruta

Puede proteger las cuentas de la aplicación frente a ataques de diccionario habilitando el bloqueo de usuario. El código siguiente del método ApplicationUserManager Create configura el bloqueo:

// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;

El código anterior solo habilita el bloqueo para la autenticación en dos fases. Aunque puede habilitar el bloqueo para los inicios de sesión cambiando shouldLockout a true en el método Login del controlador de cuenta, se recomienda no habilitar el bloqueo para los inicios de sesión porque hace que la cuenta sea susceptible a ataques de inicio de sesión de DOS. En el código de ejemplo, el bloqueo está deshabilitado para la cuenta de administrador creada en el método ApplicationDbInitializer Seed:

public static void InitializeIdentityForEF(ApplicationDbContext db)
{
    var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
    var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
    const string name = "admin@example.com";
    const string roleName = "Admin";

    //Create Role Admin if it does not exist
    var role = roleManager.FindByName(roleName);
    if (role == null)
    {
        role = new IdentityRole(roleName);
        var roleresult = roleManager.Create(role);
    }

    var user = userManager.FindByName(name);
    if (user == null)
    {
        user = new ApplicationUser { UserName = name, Email = name };
        var result = userManager.Create(user, GetSecurePassword());
        result = userManager.SetLockoutEnabled(user.Id, false);
    }

    // Add user admin to Role Admin if not already added
    var rolesForUser = userManager.GetRoles(user.Id);
    if (!rolesForUser.Contains(role.Name))
    {
        var result = userManager.AddToRole(user.Id, role.Name);
    }
}

Requerir que un usuario tenga una cuenta de correo electrónico validada

El código siguiente requiere que un usuario tenga una cuenta de correo electrónico validada para poder iniciar sesión:

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 doen't count login failures towards lockout only two factor authentication
    // To enable password failures to trigger 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 });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

Cómo comprueba SignInManager los requisitos de 2FA

Tanto el inicio de sesión local como el inicio de sesión social comprueban si 2FA está activado. Si 2FA está habilitado, el método de inicio de sesión SignInManager devuelve SignInStatus.RequiresVerification, y el usuario se redirigirá al método de acción SendCode, donde tendrá que escribir el código para completar el registro en secuencia. Si el usuario tiene RememberMe establecido en la cookie local de los usuarios, SignInManager devolverá SignInStatus.Success y no tendrá que pasar por 2FA.

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 doen't count login failures towards lockout only two factor authentication
    // To enable password failures to trigger 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 });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
    {
        return RedirectToAction("Login");
    }

    // Sign in the user with this external login provider if the user already has a login
    var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
    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:
            // If the user does not have an account, then prompt the user to create an account
            ViewBag.ReturnUrl = returnUrl;
            ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
            return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
    }
}

El código siguiente muestra el método de acción SendCode. Se crea SelectListItem con todos los métodos 2FA habilitados para el usuario. SelectListItem se pasa al asistente DropDownListFor, lo que permite al usuario seleccionar el enfoque 2FA (normalmente correo electrónico y SMS).

public async Task<ActionResult> SendCode(string returnUrl)
{
    var userId = await SignInManager.GetVerifiedUserIdAsync();
    if (userId == null)
    {
        return View("Error");
    }
    var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId);
    var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
    return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl });
}

Una vez que el usuario publica el enfoque de 2FA, se llama al método de acción HTTP POST SendCode, SignInManager envía el código 2FA y el usuario se redirige al método de acción VerifyCode donde puede escribir el código para completar el inicio de sesión.

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

    // Generate the token and send it
    if (!await SignInManager.SendTwoFactorCodeAsync(model.SelectedProvider))
    {
        return View("Error");
    }
    return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl });
}

Bloqueo de 2FA

Aunque puede establecer el bloqueo de cuenta en los errores de intentos de contraseña de inicio de sesión, ese enfoque hace que el inicio de sesión sea susceptible a los bloqueos de DOS. Se recomienda usar el bloqueo de cuenta solo con 2FA. Cuando se crea ApplicationUserManager, el código de ejemplo establece el bloqueo 2FA y MaxFailedAccessAttemptsBeforeLockout en cinco. Una vez que un usuario inicia sesión (a través de una cuenta local o una cuenta social), cada intento con error en 2FA se almacena y, si se alcanza el máximo de intentos, el usuario se bloquea durante cinco minutos (puede establecer el tiempo de bloqueo con DefaultAccountLockoutTimeSpan).

Recursos adicionales