Compartir a través de


Autenticación multifactor en ASP.NET Core

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulta la versión .NET 8 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

De Damien Bowden

Vea o descargue el código de ejemplo (repositorio de GitHub damienbod/AspNetCoreHybridFlowWithApi)

La autenticación multifactor (MFA) es un proceso por el que, durante un evento de inicio de sesión, se solicitan a un usuario otras formas de identificación. Esta solicitud puede ser para introducir un código desde un teléfono móvil, usar una clave FIDO2 o proporcionar un escaneado de huellas dactilares. Cuando se requiere una segunda forma de autenticación, la seguridad aumenta. Un ciberdelincuente no obtiene ni duplica fácilmente el factor adicional.

Este artículo cubre las siguientes áreas:

  • ¿Qué es MFA y qué flujos de MFA se recomiendan?
  • Configuración de MFA para páginas de administración mediante ASP.NET Core Identity
  • Envío del requisito de inicio de sesión de MFA al servidor de OpenID Connect
  • Forzar al cliente de ASP.NET Core OpenID Connect a requerir MFA

MFA, 2FA

MFA requiere al menos dos o más tipos de prueba para una identity como por ejemplo algo que sabes, algo que posees, o una validación biométrica para que el usuario se autentique.

La autenticación en dos fases (2FA) es como un subconjunto de MFA, pero la diferencia es que MFA puede requerir dos o más factores para demostrar la identity.

2FA se admite de forma predeterminada cuando se usa ASP.NET Core Identity. Para habilitar o deshabilitar 2FA para un usuario específico, establezca la propiedad IdentityUser<TKey>.TwoFactorEnabled. La interfaz de usuario predeterminada de ASP.NET Core Identity incluye páginas para configurar 2FA.

MFA TOTP (algoritmo de contraseña de un solo uso basado en tiempo)

MFA usando TOTP es compatible de manera predeterminada cuando se usa ASP.NET Core Identity. Este enfoque se puede usar junto con cualquier aplicación autenticadora compatible, entre las que se incluyen:

  • Microsoft Authenticator
  • Google Authenticator

Para conocer los detalles de la implementación, consulte Habilitar la generación de códigos QR para aplicaciones con autenticador TOTP en ASP.NET Core.

Para deshabilitar la compatibilidad con MFA TOTP, configure la autenticación mediante AddIdentity en lugar de AddDefaultIdentity. AddDefaultIdentity llama internamente a AddDefaultTokenProviders, que registra varios proveedores de tokens, incluido uno para MFA TOTP. Para registrar solo proveedores de tokens específicos, llame a AddTokenProvider para cada proveedor necesario. Para más información sobre los proveedores de tokens disponibles, consulte el origen de AddDefaultTokenProviders en GitHub.

Claves de paso de MFA/FIDO2 o sin contraseña

Clave de paso/FIDO2 está actualmente:

  • La forma más segura de lograr MFA.
  • MFA que protege contra ataques de suplantación de identidad (phishing). (Así como la autenticación de certificados y Windows para empresas)

En la actualidad, ASP.NET Core no admite claves de paso/FIDO2 directamente. Claves de paso/FIDO2 se puede usar para MFA o flujos sin contraseña.

Microsoft Entra ID proporciona compatibilidad con flujos de claves de paso/FIDO2 y sin contraseña. Para más información, consulte Opciones de autenticación sin contraseña.

Otras formas de MFA sin contraseña no protegen contra la suplantación de identidad (phishing).

MFA SMS

MFA con SMS aumenta la seguridad masivamente en comparación con la autenticación de contraseñas (factor único). Sin embargo, ya no se recomienda usar SMS como segundo factor. Existen demasiados vectores de ataque conocidos para este tipo de implementación.

Directrices de NIST

Configuración de MFA para páginas de administración mediante ASP.NET Core Identity

Se podría forzar la MFA a los usuarios para acceder a páginas confidenciales dentro de una aplicación de ASP.NET Core Identity. Esto podría ser útil para las aplicaciones en las que existen distintos niveles de acceso para las distintas identidades. Por ejemplo, es posible que los usuarios puedan ver los datos del perfil mediante un inicio de sesión de contraseña, pero un administrador tendría que usar MFA para acceder a las páginas administrativas.

Extensión del inicio de sesión con una notificación de MFA

El código de demostración se configura mediante ASP.NET Core con Identity y Razor Pages. El método AddIdentity se usa en lugar del AddDefaultIdentity, por lo que se puede usar una implementación de IUserClaimsPrincipalFactory para agregar notificaciones a la identity después de un inicio de sesión correcto.

Advertencia

En este artículo se muestra el uso de cadena de conexión. Con una base de datos local, el usuario no tiene que autenticarse, pero en producción, cadena de conexión a veces incluye una contraseña para autenticarse. Una credencial de contraseña de propietario de recursos (ROPC) es un riesgo de seguridad que se debe evitar en las bases de datos de producción. Las aplicaciones de producción deben usar el flujo de autenticación más seguro disponible. Para obtener más información sobre la autenticación de aplicaciones implementadas para probar o entornos de producción, consulte Flujos de autenticación seguros.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
		options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender, EmailSender>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
    AdditionalUserClaimsPrincipalFactory>();

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled", x => x.RequireClaim("amr", "mfa")));

builder.Services.AddRazorPages();

La clase AdditionalUserClaimsPrincipalFactory agrega la notificación amr a las notificaciones del usuario solo después de un inicio de sesión correcto. El valor de la notificación se lee de la base de datos. La notificación se agrega aquí porque el usuario solo debe acceder a la vista protegida superior si la identity ha iniciado sesión con MFA. Si la vista de base de datos se lee directamente desde la base de datos en lugar de usar la notificación, es posible acceder a la vista sin MFA directamente después de activar la MFA.

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Dado que la configuración del servicio Identity ha cambiado en la clase Startup, es necesario actualizar las distribuciones de Identity. Aplique scaffolding a las páginas de Identity en la aplicación. Defina el diseño en el archivo Identity/Account/Manage/_Layout.cshtml.

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

Asigne también el diseño de todas las páginas de administración de las páginas de Identity:

@{
    Layout = "_Layout.cshtml";
}

Validación del requisito de MFA en la página de administración

La Razor Page de administración valida que el usuario ha iniciado sesión con MFA. En el método OnGet, se usa la identity para acceder a las notificaciones de los usuarios. La notificación amr se comprueba para el valor mfa. Si falta la identity de esta notificación o es false, la página redirige a la página Habilitar MFA. Esto es posible porque el usuario ya ha iniciado sesión, pero sin MFA.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

Lógica de la interfaz de usuario para alternar la información de inicio de sesión del usuario

Se agregó una directiva de autorización al inicio. La directiva requiere la notificación de amr con el valor mfa.

services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));

Esta directiva puede usarse después en la vista _Layout para mostrar u ocultar el menú de Administración con la advertencia:

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService

Si la identity ha iniciado sesión usando MFA, el menú de Administración se muestra sin la advertencia de información sobre herramientas. Cuando el usuario ha iniciado sesión sin MFA, se muestra el menú Administración (no habilitado) junto con la información sobre herramientas que informa al usuario (explicando la advertencia).

@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}

Si el usuario inicia sesión sin MFA, se muestra la advertencia:

Autenticación MFA del administrador

Se redirige al usuario a la vista de habilitación de MFA al hacer clic en el vínculo de Administración:

El administrador activa la autenticación MFA

Envío del requisito de inicio de sesión de MFA al servidor de OpenID Connect

El parámetro acr_values puede usarse para pasar el valor requerido mfa del cliente al servidor en una solicitud de autenticación.

Nota

Es necesario controlar el parámetro acr_values en el servidor de OpenID Connect para que esto funcione.

Cliente de ASP.NET Core de OpenID Connect

La aplicación cliente ASP.NET Core Razor Pages OpenID Connect usa el método AddOpenIdConnect para iniciar sesión en el servidor de OpenID Connect. El parámetro acr_values se establece con el valor mfa y se envía con la solicitud de autentificación. OpenIdConnectEvents se usa para agregar esto.

Para conocer los valores de los parámetros acr_values recomendados, consulte Valores de referencia del método de autenticación.

build.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "<OpenID Connect server URL>";
	options.RequireHttpsMetadata = true;
	options.ClientId = "<OpenID Connect client ID>";
	options.ClientSecret = "<>";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
	options.AdditionalAuthorizationParameters.Add("acr_values", "mfa");
});

Ejemplo de servidor Duende IdentityServer de OpenID Connect con Identity de ASP.NET Core

En el servidor de OpenID Connect, que se implementa usando ASP.NET Core Identity con Razor Pages, se crea una nueva página llamada ErrorEnable2FA.cshtml. La vista:

  • Muestra si el Identity proviene de una app que requiere MFA pero el usuario no la ha activado en Identity.
  • Informa al usuario y agrega un vínculo para activarla.
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a href="~/Identity/Account/Manage/TwoFactorAuthentication">Enable MFA</a>

En el método Login, se usa la implementación IIdentityServerInteractionService de la interfaz _interaction para acceder a los parámetros de solicitud de OpenID Connect. Se accede al parámetro acr_values usando la propiedad AcrValues. Como el cliente lo envió con mfa establecido, se puede comprobar.

Si se requiere MFA y el usuario de ASP.NET Core Identity tiene MFA habilitada, el inicio de sesión continúa. Cuando el usuario no tiene habilitada MFA, se redirige al usuario a la vista personalizada ErrorEnable2FA.cshtml. A continuación, ASP.NET Core Identity inicia la sesión del usuario.

Fido2Store se usa para comprobar si el usuario ha activado MFA mediante un proveedor de tokens FIDO2 personalizado.

public async Task<IActionResult> OnPost()
{
	// check if we are in the context of an authorization request
	var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);

	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	var user = await _userManager.FindByNameAsync(Input.Username);
	if (user != null && !user.TwoFactorEnabled && requires2Fa)
	{
		return RedirectToPage("/Home/ErrorEnable2FA/Index");
	}

	// code omitted for brevity

	if (ModelState.IsValid)
	{
		var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberLogin, lockoutOnFailure: true);
		if (result.Succeeded)
		{
			// code omitted for brevity
		}
		if (result.RequiresTwoFactor)
		{
			var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(user.UserName);
			if (fido2ItemExistsForUser.Count > 0)
			{
				return RedirectToPage("/Account/LoginFido2Mfa", new { area = "Identity", Input.ReturnUrl, Input.RememberLogin });
			}

			return RedirectToPage("/Account/LoginWith2fa", new { area = "Identity", Input.ReturnUrl, RememberMe = Input.RememberLogin });
		}

		await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId: context?.Client.ClientId));
		ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
	}

	// something went wrong, show form with error
	await BuildModelAsync(Input.ReturnUrl);
	return Page();
}

Si el usuario ya ha iniciado sesión, la aplicación cliente:

  • Sigue validando la notificación amr.
  • Puede configurar la MFA con un vínculo a la vista de ASP.NET Core Identity.

imagen de acr_values-1

Forzar al cliente de ASP.NET Core OpenID Connect a requerir MFA

Este ejemplo muestra cómo una aplicación de ASP.NET Core Razor Page, que usa OpenID Connect para iniciar sesión, puede requerir que los usuarios se hayan autenticado usando MFA.

Para validar el requisito de MFA, se crea un requisito IAuthorizationRequirement. Esto se agregará a las páginas mediante una directiva que requiera MFA.

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

Se implementa un AuthorizationHandler que usará la notificación amr y comprobará el valor mfa. El amr se devuelve en el id_token de una autenticación con éxito y puede tener muchos valores diferentes tal y como se define en la especificación Valores de referencia del método de autenticación.

El valor devuelto depende de cómo se autentique la identity y de la implementación del servidor de OpenID Connect.

El AuthorizationHandler usa el requisito RequireMfa y valida la notificación amr. El servidor de OpenID Connect se puede implementar usando Duende Identity Server con ASP.NET Core Identity. Cuando un usuario se conecta usando TOTP, la notificación amr se devuelve con un valor de MFA. Si se usa una implementación del servidor de OpenID Connect diferente o un tipo de MFA distinto, la notificación amr tendrá, o puede tener, un valor diferente. El código debe extenderse para aceptarlo también.

public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}

En el archivo de programa, se usa el método AddOpenIdConnect como esquema de desafío predeterminado. El controlador de autorización, que se usa para comprobar la notificación amr, se agrega al contenedor de inversión de control. Después se crea una directiva que agrega el requisito RequireMfa.

builder.Services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

builder.Services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

builder.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "https://localhost:44352";
	options.RequireHttpsMetadata = true;
	options.ClientId = "AspNetCoreRequireMfaOidc";
	options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
});

builder.Services.AddAuthorization(options =>
{
	options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
	{
		policyIsAdminRequirement.Requirements.Add(new RequireMfa());
	});
});

builder.Services.AddRazorPages();

Esta directiva se usará después en la página de Razor según sea necesario. La directiva también se podría agregar globalmente para toda la aplicación.

[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

Si el usuario se autentica sin MFA, la notificación amr tendrá probablemente un valor pwd. La solicitud no se autorizará para acceder a la página. Si se usan los valores predeterminados, se redirigirá al usuario a la página Account/AccessDenied. Este comportamiento se puede cambiar o puede implementar su propia lógica personalizada aquí. En este ejemplo, se agrega un vínculo para que el usuario válido pueda configurar MFA para su cuenta.

@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>

Ahora solo los usuarios que se autentican con MFA pueden acceder a la página o el sitio web. Si se usan diferentes tipos de MFA o si 2FA está bien, la notificación amr tendrá valores diferentes y deberá procesarse correctamente. Los distintos servidores de OpenID Connect también devuelven valores diferentes para esta notificación y podrían no seguir la especificación Valores de referencia del método de autenticación.

Al iniciar sesión sin MFA (por ejemplo, con solo una contraseña):

  • El amr tiene el valor pwd:

    amr tiene el valor de pwd

  • Se deniega el acceso:

    Se denegó el acceso

Como alternativa, inicie sesión con OTP con Identity:

Inicio de sesión usando OTP con Identity

Personalización de parámetros de OIDC y OAuth

La opción de los controladores de autenticación OAuth y OIDC AdditionalAuthorizationParameters permite personalizar los parámetros de los mensajes de autorización que suelen incluirse como parte de la cadena de consulta de redireccionamiento:

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});

Recursos adicionales

Por Damien Bowden

Vea o descargue el código de ejemplo (repositorio de GitHub damienbod/AspNetCoreHybridFlowWithApi)

La autenticación multifactor (MFA) es un proceso por el que, durante un evento de inicio de sesión, se solicitan a un usuario otras formas de identificación. Esta solicitud puede ser para introducir un código desde un teléfono móvil, usar una clave FIDO2 o proporcionar un escaneado de huellas dactilares. Cuando se requiere una segunda forma de autenticación, la seguridad aumenta. Un ciberdelincuente no obtiene ni duplica fácilmente el factor adicional.

Este artículo cubre las siguientes áreas:

  • ¿Qué es MFA y qué flujos de MFA se recomiendan?
  • Configuración de MFA para páginas de administración mediante ASP.NET Core Identity
  • Envío del requisito de inicio de sesión de MFA al servidor de OpenID Connect
  • Forzar al cliente de ASP.NET Core OpenID Connect a requerir MFA

MFA, 2FA

MFA requiere al menos dos o más tipos de prueba para una identity como por ejemplo algo que sabes, algo que posees, o una validación biométrica para que el usuario se autentique.

La autenticación en dos fases (2FA) es como un subconjunto de MFA, pero la diferencia es que MFA puede requerir dos o más factores para demostrar la identity.

2FA se admite de forma predeterminada cuando se usa ASP.NET Core Identity. Para habilitar o deshabilitar 2FA para un usuario específico, establezca la propiedad IdentityUser<TKey>.TwoFactorEnabled. La interfaz de usuario predeterminada de ASP.NET Core Identity incluye páginas para configurar 2FA.

MFA TOTP (algoritmo de contraseña de un solo uso basado en tiempo)

MFA usando TOTP es compatible de manera predeterminada cuando se usa ASP.NET Core Identity. Este enfoque se puede usar junto con cualquier aplicación autenticadora compatible, entre las que se incluyen:

  • Microsoft Authenticator
  • Google Authenticator

Para conocer los detalles de la implementación, consulte Habilitar la generación de códigos QR para aplicaciones con autenticador TOTP en ASP.NET Core.

Para deshabilitar la compatibilidad con MFA TOTP, configure la autenticación mediante AddIdentity en lugar de AddDefaultIdentity. AddDefaultIdentity llama internamente a AddDefaultTokenProviders, que registra varios proveedores de tokens, incluido uno para MFA TOTP. Para registrar solo proveedores de tokens específicos, llame a AddTokenProvider para cada proveedor necesario. Para más información sobre los proveedores de tokens disponibles, consulte el origen de AddDefaultTokenProviders en GitHub.

Claves de paso de MFA/FIDO2 o sin contraseña

Clave de paso/FIDO2 está actualmente:

  • La forma más segura de lograr MFA.
  • MFA que protege contra ataques de suplantación de identidad (phishing). (Así como la autenticación de certificados y Windows para empresas)

En la actualidad, ASP.NET Core no admite claves de paso/FIDO2 directamente. Claves de paso/FIDO2 se puede usar para MFA o flujos sin contraseña.

Microsoft Entra ID proporciona compatibilidad con flujos de claves de paso/FIDO2 y sin contraseña. Para más información, consulte Opciones de autenticación sin contraseña.

Otras formas de MFA sin contraseña no protegen contra la suplantación de identidad (phishing).

MFA SMS

MFA con SMS aumenta la seguridad masivamente en comparación con la autenticación de contraseñas (factor único). Sin embargo, ya no se recomienda usar SMS como segundo factor. Existen demasiados vectores de ataque conocidos para este tipo de implementación.

Directrices de NIST

Configuración de MFA para páginas de administración mediante ASP.NET Core Identity

Se podría forzar la MFA a los usuarios para acceder a páginas confidenciales dentro de una aplicación de ASP.NET Core Identity. Esto podría ser útil para las aplicaciones en las que existen distintos niveles de acceso para las distintas identidades. Por ejemplo, es posible que los usuarios puedan ver los datos del perfil mediante un inicio de sesión de contraseña, pero un administrador tendría que usar MFA para acceder a las páginas administrativas.

Extensión del inicio de sesión con una notificación de MFA

El código de demostración se configura mediante ASP.NET Core con Identity y Razor Pages. El método AddIdentity se usa en lugar del AddDefaultIdentity, por lo que se puede usar una implementación de IUserClaimsPrincipalFactory para agregar notificaciones a la identity después de un inicio de sesión correcto.

Advertencia

En este artículo se muestra el uso de cadena de conexión. Con una base de datos local, el usuario no tiene que autenticarse, pero en producción, cadena de conexión a veces incluye una contraseña para autenticarse. Una credencial de contraseña de propietario de recursos (ROPC) es un riesgo de seguridad que se debe evitar en las bases de datos de producción. Las aplicaciones de producción deben usar el flujo de autenticación más seguro disponible. Para obtener más información sobre la autenticación de aplicaciones implementadas para probar o entornos de producción, consulte Flujos de autenticación seguros.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
		options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender, EmailSender>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
    AdditionalUserClaimsPrincipalFactory>();

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled", x => x.RequireClaim("amr", "mfa")));

builder.Services.AddRazorPages();

La clase AdditionalUserClaimsPrincipalFactory agrega la notificación amr a las notificaciones del usuario solo después de un inicio de sesión correcto. El valor de la notificación se lee de la base de datos. La notificación se agrega aquí porque el usuario solo debe acceder a la vista protegida superior si la identity ha iniciado sesión con MFA. Si la vista de base de datos se lee directamente desde la base de datos en lugar de usar la notificación, es posible acceder a la vista sin MFA directamente después de activar la MFA.

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Dado que la configuración del servicio Identity ha cambiado en la clase Startup, es necesario actualizar las distribuciones de Identity. Aplique scaffolding a las páginas de Identity en la aplicación. Defina el diseño en el archivo Identity/Account/Manage/_Layout.cshtml.

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

Asigne también el diseño de todas las páginas de administración de las páginas de Identity:

@{
    Layout = "_Layout.cshtml";
}

Validación del requisito de MFA en la página de administración

La Razor Page de administración valida que el usuario ha iniciado sesión con MFA. En el método OnGet, se usa la identity para acceder a las notificaciones de los usuarios. La notificación amr se comprueba para el valor mfa. Si falta la identity de esta notificación o es false, la página redirige a la página Habilitar MFA. Esto es posible porque el usuario ya ha iniciado sesión, pero sin MFA.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

Lógica de la interfaz de usuario para alternar la información de inicio de sesión del usuario

Se agregó una directiva de autorización al inicio. La directiva requiere la notificación de amr con el valor mfa.

services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));

Esta directiva puede usarse después en la vista _Layout para mostrar u ocultar el menú de Administración con la advertencia:

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService

Si la identity ha iniciado sesión usando MFA, el menú de Administración se muestra sin la advertencia de información sobre herramientas. Cuando el usuario ha iniciado sesión sin MFA, se muestra el menú Administración (no habilitado) junto con la información sobre herramientas que informa al usuario (explicando la advertencia).

@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}

Si el usuario inicia sesión sin MFA, se muestra la advertencia:

Autenticación MFA del administrador

Se redirige al usuario a la vista de habilitación de MFA al hacer clic en el vínculo de Administración:

El administrador activa la autenticación MFA

Envío del requisito de inicio de sesión de MFA al servidor de OpenID Connect

El parámetro acr_values puede usarse para pasar el valor requerido mfa del cliente al servidor en una solicitud de autenticación.

Nota

Es necesario controlar el parámetro acr_values en el servidor de OpenID Connect para que esto funcione.

Cliente de ASP.NET Core de OpenID Connect

La aplicación cliente ASP.NET Core Razor Pages OpenID Connect usa el método AddOpenIdConnect para iniciar sesión en el servidor de OpenID Connect. El parámetro acr_values se establece con el valor mfa y se envía con la solicitud de autentificación. OpenIdConnectEvents se usa para agregar esto.

Para conocer los valores de los parámetros acr_values recomendados, consulte Valores de referencia del método de autenticación.

build.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "<OpenID Connect server URL>";
	options.RequireHttpsMetadata = true;
	options.ClientId = "<OpenID Connect client ID>";
	options.ClientSecret = "<>";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
	options.Events = new OpenIdConnectEvents
	{
		OnRedirectToIdentityProvider = context =>
		{
			context.ProtocolMessage.SetParameter("acr_values", "mfa");
			return Task.FromResult(0);
		}
	};
});

Ejemplo de servidor Duende IdentityServer de OpenID Connect con Identity de ASP.NET Core

En el servidor de OpenID Connect, que se implementa usando ASP.NET Core Identity con Razor Pages, se crea una nueva página llamada ErrorEnable2FA.cshtml. La vista:

  • Muestra si el Identity proviene de una app que requiere MFA pero el usuario no la ha activado en Identity.
  • Informa al usuario y agrega un vínculo para activarla.
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a href="~/Identity/Account/Manage/TwoFactorAuthentication">Enable MFA</a>

En el método Login, se usa la implementación IIdentityServerInteractionService de la interfaz _interaction para acceder a los parámetros de solicitud de OpenID Connect. Se accede al parámetro acr_values usando la propiedad AcrValues. Como el cliente lo envió con mfa establecido, se puede comprobar.

Si se requiere MFA y el usuario de ASP.NET Core Identity tiene MFA habilitada, el inicio de sesión continúa. Cuando el usuario no tiene habilitada MFA, se redirige al usuario a la vista personalizada ErrorEnable2FA.cshtml. A continuación, ASP.NET Core Identity inicia la sesión del usuario.

Fido2Store se usa para comprobar si el usuario ha activado MFA mediante un proveedor de tokens FIDO2 personalizado.

public async Task<IActionResult> OnPost()
{
	// check if we are in the context of an authorization request
	var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);

	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	var user = await _userManager.FindByNameAsync(Input.Username);
	if (user != null && !user.TwoFactorEnabled && requires2Fa)
	{
		return RedirectToPage("/Home/ErrorEnable2FA/Index");
	}

	// code omitted for brevity

	if (ModelState.IsValid)
	{
		var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberLogin, lockoutOnFailure: true);
		if (result.Succeeded)
		{
			// code omitted for brevity
		}
		if (result.RequiresTwoFactor)
		{
			var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(user.UserName);
			if (fido2ItemExistsForUser.Count > 0)
			{
				return RedirectToPage("/Account/LoginFido2Mfa", new { area = "Identity", Input.ReturnUrl, Input.RememberLogin });
			}

			return RedirectToPage("/Account/LoginWith2fa", new { area = "Identity", Input.ReturnUrl, RememberMe = Input.RememberLogin });
		}

		await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId: context?.Client.ClientId));
		ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
	}

	// something went wrong, show form with error
	await BuildModelAsync(Input.ReturnUrl);
	return Page();
}

Si el usuario ya ha iniciado sesión, la aplicación cliente:

  • Sigue validando la notificación amr.
  • Puede configurar la MFA con un vínculo a la vista de ASP.NET Core Identity.

imagen de acr_values-1

Forzar al cliente de ASP.NET Core OpenID Connect a requerir MFA

Este ejemplo muestra cómo una aplicación de ASP.NET Core Razor Page, que usa OpenID Connect para iniciar sesión, puede requerir que los usuarios se hayan autenticado usando MFA.

Para validar el requisito de MFA, se crea un requisito IAuthorizationRequirement. Esto se agregará a las páginas mediante una directiva que requiera MFA.

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

Se implementa un AuthorizationHandler que usará la notificación amr y comprobará el valor mfa. El amr se devuelve en el id_token de una autenticación con éxito y puede tener muchos valores diferentes tal y como se define en la especificación Valores de referencia del método de autenticación.

El valor devuelto depende de cómo se autentique la identity y de la implementación del servidor de OpenID Connect.

El AuthorizationHandler usa el requisito RequireMfa y valida la notificación amr. El servidor de OpenID Connect se puede implementar usando Duende Identity Server con ASP.NET Core Identity. Cuando un usuario se conecta usando TOTP, la notificación amr se devuelve con un valor de MFA. Si se usa una implementación del servidor de OpenID Connect diferente o un tipo de MFA distinto, la notificación amr tendrá, o puede tener, un valor diferente. El código debe extenderse para aceptarlo también.

public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}

En el archivo de programa, se usa el método AddOpenIdConnect como esquema de desafío predeterminado. El controlador de autorización, que se usa para comprobar la notificación amr, se agrega al contenedor de inversión de control. Después se crea una directiva que agrega el requisito RequireMfa.

builder.Services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

builder.Services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

builder.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "https://localhost:44352";
	options.RequireHttpsMetadata = true;
	options.ClientId = "AspNetCoreRequireMfaOidc";
	options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
});

builder.Services.AddAuthorization(options =>
{
	options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
	{
		policyIsAdminRequirement.Requirements.Add(new RequireMfa());
	});
});

builder.Services.AddRazorPages();

Esta directiva se usará después en la página de Razor según sea necesario. La directiva también se podría agregar globalmente para toda la aplicación.

[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

Si el usuario se autentica sin MFA, la notificación amr tendrá probablemente un valor pwd. La solicitud no se autorizará para acceder a la página. Si se usan los valores predeterminados, se redirigirá al usuario a la página Account/AccessDenied. Este comportamiento se puede cambiar o puede implementar su propia lógica personalizada aquí. En este ejemplo, se agrega un vínculo para que el usuario válido pueda configurar MFA para su cuenta.

@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>

Ahora solo los usuarios que se autentican con MFA pueden acceder a la página o el sitio web. Si se usan diferentes tipos de MFA o si 2FA está bien, la notificación amr tendrá valores diferentes y deberá procesarse correctamente. Los distintos servidores de OpenID Connect también devuelven valores diferentes para esta notificación y podrían no seguir la especificación Valores de referencia del método de autenticación.

Al iniciar sesión sin MFA (por ejemplo, con solo una contraseña):

  • El amr tiene el valor pwd:

    amr tiene el valor de pwd

  • Se deniega el acceso:

    Se denegó el acceso

Como alternativa, inicie sesión con OTP con Identity:

Inicio de sesión usando OTP con Identity

Recursos adicionales

Por Damien Bowden

Vea o descargue el código de ejemplo (repositorio de GitHub damienbod/AspNetCoreHybridFlowWithApi)

La autenticación multifactor (MFA) es un proceso por el que, durante un evento de inicio de sesión, se solicitan a un usuario otras formas de identificación. Esta solicitud puede ser para introducir un código desde un teléfono móvil, usar una clave FIDO2 o proporcionar un escaneado de huellas dactilares. Cuando se requiere una segunda forma de autenticación, la seguridad aumenta. Un ciberdelincuente no obtiene ni duplica fácilmente el factor adicional.

Este artículo cubre las siguientes áreas:

  • ¿Qué es MFA y qué flujos de MFA se recomiendan?
  • Configuración de MFA para páginas de administración mediante ASP.NET Core Identity
  • Envío del requisito de inicio de sesión de MFA al servidor de OpenID Connect
  • Forzar al cliente de ASP.NET Core OpenID Connect a requerir MFA

MFA, 2FA

MFA requiere al menos dos o más tipos de prueba para una identity como por ejemplo algo que sabes, algo que posees, o una validación biométrica para que el usuario se autentique.

La autenticación en dos fases (2FA) es como un subconjunto de MFA, pero la diferencia es que MFA puede requerir dos o más factores para demostrar la identity.

MFA TOTP (algoritmo de contraseña de un solo uso basado en tiempo)

MFA mediante TOTP es una implementación compatible mediante ASP.NET Core Identity. Esto se puede usar junto con cualquier aplicación autenticadora compatible, entre las que se incluyen:

  • Aplicación Microsoft Authenticator
  • Aplicación Google Authenticator

Consulte el siguiente vínculo para obtener detalles de implementación:

Habilitación de la generación de códigos QR para las aplicaciones de autenticación TOTP en ASP.NET Core

Claves de paso de MFA/FIDO2 o sin contraseña

Clave de paso/FIDO2 está actualmente:

  • La forma más segura de lograr MFA.
  • MFA que protege contra ataques de suplantación de identidad (phishing). (Así como la autenticación de certificados y Windows para empresas)

En la actualidad, ASP.NET Core no admite claves de paso/FIDO2 directamente. Claves de paso/FIDO2 se puede usar para MFA o flujos sin contraseña.

Microsoft Entra ID proporciona compatibilidad con flujos de claves de paso/FIDO2 y sin contraseña. Para más información, consulte Opciones de autenticación sin contraseña.

Otras formas de MFA sin contraseña no protegen contra la suplantación de identidad (phishing).

MFA SMS

MFA con SMS aumenta la seguridad masivamente en comparación con la autenticación de contraseñas (factor único). Sin embargo, ya no se recomienda usar SMS como segundo factor. Existen demasiados vectores de ataque conocidos para este tipo de implementación.

Directrices de NIST

Configuración de MFA para páginas de administración mediante ASP.NET Core Identity

Se podría forzar la MFA a los usuarios para acceder a páginas confidenciales dentro de una aplicación de ASP.NET Core Identity. Esto podría ser útil para las aplicaciones en las que existen distintos niveles de acceso para las distintas identidades. Por ejemplo, es posible que los usuarios puedan ver los datos del perfil mediante un inicio de sesión de contraseña, pero un administrador tendría que usar MFA para acceder a las páginas administrativas.

Extensión del inicio de sesión con una notificación de MFA

El código de demostración se configura mediante ASP.NET Core con Identity y Razor Pages. El método AddIdentity se usa en lugar del AddDefaultIdentity, por lo que se puede usar una implementación de IUserClaimsPrincipalFactory para agregar notificaciones a la identity después de un inicio de sesión correcto.

Advertencia

En este artículo se muestra el uso de cadena de conexión. Con una base de datos local, el usuario no tiene que autenticarse, pero en producción, cadena de conexión a veces incluye una contraseña para autenticarse. Una credencial de contraseña de propietario de recursos (ROPC) es un riesgo de seguridad que se debe evitar en las bases de datos de producción. Las aplicaciones de producción deben usar el flujo de autenticación más seguro disponible. Para obtener más información sobre la autenticación de aplicaciones implementadas para probar o entornos de producción, consulte Flujos de autenticación seguros.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<IdentityUser, IdentityRole>(
            options => options.SignIn.RequireConfirmedAccount = false)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddSingleton<IEmailSender, EmailSender>();
    services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
        AdditionalUserClaimsPrincipalFactory>();

    services.AddAuthorization(options =>
        options.AddPolicy("TwoFactorEnabled",
            x => x.RequireClaim("amr", "mfa")));

    services.AddRazorPages();
}

La clase AdditionalUserClaimsPrincipalFactory agrega la notificación amr a las notificaciones del usuario solo después de un inicio de sesión correcto. El valor de la notificación se lee de la base de datos. La notificación se agrega aquí porque el usuario solo debe acceder a la vista protegida superior si la identity ha iniciado sesión con MFA. Si la vista de base de datos se lee directamente desde la base de datos en lugar de usar la notificación, es posible acceder a la vista sin MFA directamente después de activar la MFA.

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Dado que la configuración del servicio Identity ha cambiado en la clase Startup, es necesario actualizar las distribuciones de Identity. Aplique scaffolding a las páginas de Identity en la aplicación. Defina el diseño en el archivo Identity/Account/Manage/_Layout.cshtml.

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

Asigne también el diseño de todas las páginas de administración de las páginas de Identity:

@{
    Layout = "_Layout.cshtml";
}

Validación del requisito de MFA en la página de administración

La Razor Page de administración valida que el usuario ha iniciado sesión con MFA. En el método OnGet, se usa la identity para acceder a las notificaciones de los usuarios. La notificación amr se comprueba para el valor mfa. Si falta la identity de esta notificación o es false, la página redirige a la página Habilitar MFA. Esto es posible porque el usuario ya ha iniciado sesión, pero sin MFA.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

Lógica de la interfaz de usuario para alternar la información de inicio de sesión del usuario

Se agregó una directiva de autorización en el archivo de programa. La directiva requiere la notificación de amr con el valor mfa.

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));

Esta directiva puede usarse después en la vista _Layout para mostrar u ocultar el menú de Administración con la advertencia:

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService

Si la identity ha iniciado sesión usando MFA, el menú de Administración se muestra sin la advertencia de información sobre herramientas. Cuando el usuario ha iniciado sesión sin MFA, se muestra el menú Administración (no habilitado) junto con la información sobre herramientas que informa al usuario (explicando la advertencia).

@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}

Si el usuario inicia sesión sin MFA, se muestra la advertencia:

Autenticación MFA del administrador

Se redirige al usuario a la vista de habilitación de MFA al hacer clic en el vínculo de Administración:

El administrador activa la autenticación MFA

Envío del requisito de inicio de sesión de MFA al servidor de OpenID Connect

El parámetro acr_values puede usarse para pasar el valor requerido mfa del cliente al servidor en una solicitud de autenticación.

Nota

Es necesario controlar el parámetro acr_values en el servidor de OpenID Connect para que esto funcione.

Cliente de ASP.NET Core de OpenID Connect

La aplicación cliente ASP.NET Core Razor Pages OpenID Connect usa el método AddOpenIdConnect para iniciar sesión en el servidor de OpenID Connect. El parámetro acr_values se establece con el valor mfa y se envía con la solicitud de autentificación. OpenIdConnectEvents se usa para agregar esto.

Para conocer los valores de los parámetros acr_values recomendados, consulte Valores de referencia del método de autenticación.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme =
            OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.SignInScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "<OpenID Connect server URL>";
        options.RequireHttpsMetadata = true;
        options.ClientId = "<OpenID Connect client ID>";
        options.ClientSecret = "<>";
        options.ResponseType = "code";
        options.UsePkce = true;	
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
        options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProvider = context =>
            {
                context.ProtocolMessage.SetParameter("acr_values", "mfa");
                return Task.FromResult(0);
            }
        };
    });

Ejemplo de servidor IdentityServer 4 de OpenID Connect con Identity de ASP.NET Core

En el servidor de OpenID Connect, que se implementa usando ASP.NET Core Identity con vistas MVC, se crea una nueva vista llamada ErrorEnable2FA.cshtml. La vista:

  • Muestra si el Identity proviene de una app que requiere MFA pero el usuario no la ha activado en Identity.
  • Informa al usuario y agrega un vínculo para activarla.
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a asp-controller="Manage" asp-action="TwoFactorAuthentication">Enable MFA</a>

En el método Login, se usa la implementación IIdentityServerInteractionService de la interfaz _interaction para acceder a los parámetros de solicitud de OpenID Connect. Se accede al parámetro acr_values usando la propiedad AcrValues. Como el cliente lo envió con mfa establecido, se puede comprobar.

Si se requiere MFA y el usuario de ASP.NET Core Identity tiene MFA habilitada, el inicio de sesión continúa. Cuando el usuario no tiene habilitada MFA, se redirige al usuario a la vista personalizada ErrorEnable2FA.cshtml. A continuación, ASP.NET Core Identity inicia la sesión del usuario.

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
    var returnUrl = model.ReturnUrl;
    var context = 
        await _interaction.GetAuthorizationContextAsync(returnUrl);
    var requires2Fa = 
        context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

    var user = await _userManager.FindByNameAsync(model.Email);
    if (user != null && !user.TwoFactorEnabled && requires2Fa)
    {
        return RedirectToAction(nameof(ErrorEnable2FA));
    }

    // code omitted for brevity

El método ExternalLoginCallback funciona como el inicio de sesión local Identity. La propiedad AcrValues se comprueba para el valor mfa. Si el valor mfa está presente, la MFA se fuerza antes de que se complete el inicio de sesión (por ejemplo, se redirige a la vista ErrorEnable2FA).

//
// GET: /Account/ExternalLoginCallback
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(
    string returnUrl = null,
    string remoteError = null)
{
    var context =
        await _interaction.GetAuthorizationContextAsync(returnUrl);
    var requires2Fa =
        context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

    if (remoteError != null)
    {
        ModelState.AddModelError(
            string.Empty,
            _sharedLocalizer["EXTERNAL_PROVIDER_ERROR", 
            remoteError]);
        return View(nameof(Login));
    }
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        return RedirectToAction(nameof(Login));
    }

    var email = info.Principal.FindFirstValue(ClaimTypes.Email);

    if (!string.IsNullOrEmpty(email))
    {
        var user = await _userManager.FindByNameAsync(email);
        if (user != null && !user.TwoFactorEnabled && requires2Fa)
        {
            return RedirectToAction(nameof(ErrorEnable2FA));
        }
    }

    // Sign in the user with this external login provider if the user already has a login.
    var result = await _signInManager
        .ExternalLoginSignInAsync(
            info.LoginProvider, 
            info.ProviderKey, 
            isPersistent: 
            false);

    // code omitted for brevity

Si el usuario ya ha iniciado sesión, la aplicación cliente:

  • Sigue validando la notificación amr.
  • Puede configurar la MFA con un vínculo a la vista de ASP.NET Core Identity.

imagen de acr_values-1

Forzar al cliente de ASP.NET Core OpenID Connect a requerir MFA

Este ejemplo muestra cómo una aplicación de ASP.NET Core Razor Page, que usa OpenID Connect para iniciar sesión, puede requerir que los usuarios se hayan autenticado usando MFA.

Para validar el requisito de MFA, se crea un requisito IAuthorizationRequirement. Esto se agregará a las páginas mediante una directiva que requiera MFA.

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc
{
    public class RequireMfa : IAuthorizationRequirement{}
}

Se implementa un AuthorizationHandler que usará la notificación amr y comprobará el valor mfa. El amr se devuelve en el id_token de una autenticación con éxito y puede tener muchos valores diferentes tal y como se define en la especificación Valores de referencia del método de autenticación.

El valor devuelto depende de cómo se autentique la identity y de la implementación del servidor de OpenID Connect.

El AuthorizationHandler usa el requisito RequireMfa y valida la notificación amr. El servidor de OpenID Connect se puede implementar usando IdentityServer4 con Identity de ASP.NET Core. Cuando un usuario se conecta usando TOTP, la notificación amr se devuelve con un valor de MFA. Si se usa una implementación del servidor de OpenID Connect diferente o un tipo de MFA distinto, la notificación amr tendrá, o puede tener, un valor diferente. El código debe extenderse para aceptarlo también.

public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}

En el método Startup.ConfigureServices, se usa el método AddOpenIdConnect como esquema de desafío predeterminado. El controlador de autorización, que se usa para comprobar la notificación amr, se agrega al contenedor de inversión de control. Después se crea una directiva que agrega el requisito RequireMfa.

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

    services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

    services.AddAuthentication(options =>
    {
        options.DefaultScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme =
            OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.SignInScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "https://localhost:44352";
        options.RequireHttpsMetadata = true;
        options.ClientId = "AspNetCoreRequireMfaOidc";
        options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
        options.ResponseType = "code";
        options.UsePkce = true;	
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
    });

    services.AddAuthorization(options =>
    {
        options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
        {
            policyIsAdminRequirement.Requirements.Add(new RequireMfa());
        });
    });

    services.AddRazorPages();
}

Esta directiva se usará después en la página de Razor según sea necesario. La directiva también se podría agregar globalmente para toda la aplicación.

[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

Si el usuario se autentica sin MFA, la notificación amr tendrá probablemente un valor pwd. La solicitud no se autorizará para acceder a la página. Si se usan los valores predeterminados, se redirigirá al usuario a la página Account/AccessDenied. Este comportamiento se puede cambiar o puede implementar su propia lógica personalizada aquí. En este ejemplo, se agrega un vínculo para que el usuario válido pueda configurar MFA para su cuenta.

@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>

Ahora solo los usuarios que se autentican con MFA pueden acceder a la página o el sitio web. Si se usan diferentes tipos de MFA o si 2FA está bien, la notificación amr tendrá valores diferentes y deberá procesarse correctamente. Los distintos servidores de OpenID Connect también devuelven valores diferentes para esta notificación y podrían no seguir la especificación Valores de referencia del método de autenticación.

Al iniciar sesión sin MFA (por ejemplo, con solo una contraseña):

  • El amr tiene el valor pwd:

    amr tiene el valor de pwd

  • Se deniega el acceso:

    Se denegó el acceso

Como alternativa, inicie sesión con OTP con Identity:

Inicio de sesión usando OTP con Identity

Recursos adicionales