Compartir a través de


Seguridad: Autenticación y autorización en ASP.NET Web Forms y Blazor

Sugerencia

Este contenido es un extracto del libro electrónico "Blazor for ASP.NET Web Forms Developers for Azure" (Blazor para desarrolladores de ASP.NET Web Forms), disponible en Documentación de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.

Miniatura de portada del libro electrónico Blazor para desarrolladores de ASP.NET Web Forms.

Para la migración de una aplicación de ASP.NET Web Forms a Blazor seguramente sea necesario actualizar cómo se realizan la autenticación y la autorización, siempre que en la aplicación se haya configurado la autenticación. En este capítulo se describe cómo realizar la migración desde el modelo de proveedor universal de ASP.NET Web Forms (para la pertenencia, los roles y los perfiles de usuario) y cómo trabajar con ASP.NET Core Identity desde aplicaciones Blazor. Aunque en este capítulo se tratarán los pasos y las consideraciones generales, en la documentación a la que se hace referencia se pueden encontrar pasos y scripts detallados.

Proveedores universales de ASP.NET

Desde ASP.NET 2.0, la plataforma ASP.NET Web Forms admite un modelo de proveedor para distintas características, incluida la pertenencia. El proveedor de pertenencia universal, junto con el proveedor de roles opcional, se implementa normalmente con aplicaciones de ASP.NET Web Forms. Ofrece una forma sólida y segura de administrar la autenticación y la autorización que todavía funciona correctamente en la actualidad. La oferta más reciente de estos proveedores universales está disponible como un paquete NuGet, Microsoft.AspNet.Providers.

Proveedores universales funcionan con un esquema de base de datos SQL que incluye tablas como aspnet_Applications, aspnet_Membership, aspnet_Roles y aspnet_Users. Cuando se configuran mediante la ejecución del comando aspnet_regsql.exe, los proveedores instalan tablas y procedimientos almacenados que proporcionan todas las consultas y los comandos necesarios para trabajar con los datos subyacentes. El esquema de la base de datos y estos procedimientos almacenados no son compatibles con los sistemas ASP.NET Identity y ASP.NET Core Identity más recientes, por lo que los datos existentes se deben migrar al nuevo sistema. En la figura 1 se muestra un esquema de tabla de ejemplo configurado para proveedores universales.

Esquema de proveedores universales

El proveedor universal controla los usuarios, la pertenencia, los roles y los perfiles. A los usuarios se les asignan identificadores únicos globales y la información básica como userId, userName, etc., se almacenan en la tabla aspnet_Users. La información de autenticación, como la contraseña, el formato y la sal de contraseña, los contadores de bloqueo y los detalles, etc., se almacenan en la tabla aspnet_Membership. Los roles constan simplemente de nombres e identificadores únicos, que se asignan a los usuarios mediante la tabla de asociación aspnet_UsersInRoles, lo que proporciona una relación de varios a varios.

Si el sistema existente usa roles además de la pertenencia, tendrá que migrar las cuentas de usuario, las contraseñas asociadas, los roles y la pertenencia a roles a ASP.NET Core Identity. Lo más probable es que tenga que actualizar el código en el que actualmente realiza las comprobaciones de rol mediante instrucciones if para aprovechar en su lugar filtros declarativos, atributos o asistentes de etiquetas. Al final de este capítulo se revisarán las consideraciones de migración con más detalle.

Configuración de autorización en Web Forms

Para configurar el acceso autorizado a determinadas páginas en una aplicación de ASP.NET Web Forms, normalmente se especifica que los usuarios anónimos no pueden acceder determinadas páginas o carpetas anónimos. Esta configuración se realiza en el archivo web.config:

<?xml version="1.0"?>
<configuration>
    <system.web>
      <authentication mode="Forms">
        <forms defaultUrl="~/home.aspx" loginUrl="~/login.aspx"
          slidingExpiration="true" timeout="2880"></forms>
      </authentication>

      <authorization>
        <deny users="?" />
      </authorization>
    </system.web>
</configuration>

En la sección de configuración authentication se configura la autenticación de formularios para la aplicación. La sección authorization se usa para impedir usuarios anónimos en toda la aplicación. Pero puede proporcionar reglas de autorización más pormenorizadas por cada ubicación, además de aplicar comprobaciones de autorización basadas en roles.

<location path="login.aspx">
  <system.web>
    <authorization>
      <allow users="*" />
    </authorization>
  </system.web>
</location>

La configuración anterior, cuando se combina con la primera, permitiría a los usuarios anónimos acceder a la página de inicio de sesión, lo que invalidaría la restricción en todo el sitio para los usuarios no autenticados.

<location path="/admin">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

La configuración anterior, cuando se combina con las demás, restringe el acceso a la carpeta /admin y a todos los recursos que contiene a los miembros del rol "Administradores". Esta restricción también se puede aplicar si se coloca un archivo web.config independiente dentro de la raíz de la carpeta /admin.

Código de autorización en Web Forms

Además de configurar el acceso con web.config, puede configurar mediante programación el acceso y el comportamiento en la aplicación de Web Forms. Por ejemplo, puede restringir la capacidad de realizar determinadas operaciones o ver determinados datos según el rol del usuario.

Este código se puede usar en la lógica de código subyacente, así como en la propia página:

<% if (HttpContext.Current.User.IsInRole("Administrators")) { %>
  <a href="/admin">Go To Admin</a>
<% } %>

Además de comprobar la pertenencia al rol de usuario, también puede determinar si está autenticado (aunque esto normalmente se realiza mejor mediante la configuración basada en la ubicación que se ha explicado anteriormente). A continuación se muestra un ejemplo de este enfoque.

protected void Page_Load(object sender, EventArgs e)
{
    if (!User.Identity.IsAuthenticated)
    {
        FormsAuthentication.RedirectToLoginPage();
    }
    if (!Roles.IsUserInRole(User.Identity.Name, "Administrators"))
    {
        MessageLabel.Text = "Only administrators can view this.";
        SecretPanel.Visible = false;
    }
}

En el código anterior, se usa el control de acceso basado en rol (RBAC) para determinar si elementos concretos de la página, como SecretPanel, son visibles según el rol del usuario actual.

Normalmente, las aplicaciones de ASP.NET Web Forms configuran la seguridad en el archivo web.config y, después, agregan comprobaciones adicionales cuando sea necesario en las páginas .aspx y sus archivos de código subyacente .aspx.cs relacionados. La mayoría de las aplicaciones aprovechan el proveedor de pertenencia universal, habitualmente junto al proveedor de roles adicional.

Identidad de ASP.NET Core

Aunque se siga encargando de la autenticación y la autorización, en ASP.NET Core Identity se usa otro conjunto de abstracciones y suposiciones en comparación con los proveedores universales. Por ejemplo, el nuevo modelo de Identity admite la autenticación de terceros, lo que permite a los usuarios autenticarse mediante una cuenta de medios sociales u otro proveedor de autenticación de confianza. ASP.NET Core Identity admite la interfaz de usuario para las páginas que normalmente son necesarias, como las de inicio de sesión, cierre de sesión y registro. Aprovecha EF Core para su acceso a datos y usa migraciones de EF Core para generar el esquema necesario para admitir su modelo de datos. En esta Introducción a Identity en ASP.NET Core se proporciona información general de lo que se incluye con ASP.NET Core Identity y cómo empezar a trabajar con ella. Si todavía no ha configurado ASP.NET Core Identity en la aplicación y su base de datos, le ayudará a empezar.

Roles, notificaciones y directivas

Tanto los proveedores universales como ASP.NET Core Identity admiten el concepto de roles. Puede crear roles para usuarios y asignar usuarios a roles. Los usuarios pueden pertenecer a cualquier número de roles y se puede comprobar la pertenencia a roles como parte de la implementación de autorización.

Además de los roles, ASP.NET Core Identity admite los conceptos de notificaciones y directivas. Mientras que un rol se debe corresponder específicamente a un conjunto de recursos a los que un usuario de ese rol debe poder acceder, una notificación es simplemente parte de la identidad de un usuario. Una notificación es un par de valor y nombre que representa lo que es el sujeto, no lo que puede hacer.

Es posible inspeccionar directamente las notificaciones de un usuario y determinar, según estos valores, si se le debe conceder acceso a un recurso. Pero estas comprobaciones suelen ser repetitivas y dispersas por todo el sistema. Un enfoque mejor consiste en definir una directiva.

Una directiva de autorización consta de uno o varios requisitos. Las directivas se registran como parte de la configuración del servicio de autorización en el método ConfigureServices de Startup.cs. Por ejemplo, en el fragmento de código siguiente se configura una directiva denominada "CanadiansOnly", que incluye el requisito de que el usuario tenga la notificación Country con el valor "Canada".

services.AddAuthorization(options =>
{
    options.AddPolicy("CanadiansOnly", policy => policy.RequireClaim(ClaimTypes.Country, "Canada"));
});

Puede obtener más información sobre cómo crear directivas personalizadas en la documentación.

Si usa directivas o roles, puede especificar que en una página concreta de la aplicación Blazor se necesite ese rol o esa directiva con el atributo [Authorize], aplicado con la directiva @attribute.

Si se necesita un rol:

@attribute [Authorize(Roles ="administrators")]

Si se tiene que satisfacer una directiva:

@attribute [Authorize(Policy ="CanadiansOnly")]

Si necesita acceso al estado de autenticación de un usuario, a los roles o a las notificaciones en el código, hay dos formas principales de lograr esta funcionalidad. La primera consiste en recibir el estado de autenticación como un parámetro en cascada. La segunda consiste en acceder al estado mediante un elemento AuthenticationStateProvider insertado. Los detalles de cada uno de estos enfoques se describen en la documentación de seguridad de Blazor.

En el código siguiente se muestra la forma de recibir AuthenticationState como un parámetro en cascada:

[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

Con este parámetro implementado, puede obtener el usuario mediante este código:

var authState = await authenticationStateTask;
var user = authState.User;

En el código siguiente se muestra cómo insertar AuthenticationStateProvider:

@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider

Con el proveedor implementado, puede obtener acceso al usuario con el código siguiente:

AuthenticationState authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
ClaimsPrincipal user = authState.User;

if (user.Identity.IsAuthenticated)
{
  // work with user.Claims and/or user.Roles
}

Nota: El componente AuthorizeView, que se describe más adelante en este capítulo, proporciona una manera declarativa de controlar lo que un usuario ve en una página o un componente.

Para trabajar con usuarios y notificaciones (en aplicaciones Blazor Server), es posible que también tenga que insertar un objeto UserManager<T> (use IdentityUser de forma predeterminada), que puede utilizar para enumerar y modificar las notificaciones de un usuario. En primer lugar, inserte el tipo y asígnelo a una propiedad:

@inject UserManager<IdentityUser> MyUserManager

Después, úselo para trabajar con las notificaciones del usuario. En el ejemplo siguiente se muestra cómo agregar y conservar una notificación para un usuario:

private async Task AddCountryClaim()
{
    var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
    var user = authState.User;
    var identityUser = await MyUserManager.FindByNameAsync(user.Identity.Name);

    if (!user.HasClaim(c => c.Type == ClaimTypes.Country))
    {
        // stores the claim in the cookie
        ClaimsIdentity id = new ClaimsIdentity();
        id.AddClaim(new Claim(ClaimTypes.Country, "Canada"));
        user.AddIdentity(id);

        // save the claim in the database
        await MyUserManager.AddClaimAsync(identityUser, new Claim(ClaimTypes.Country, "Canada"));
    }
}

Si tiene que trabajar con roles, siga el mismo enfoque. Es posible que tenga que insertar un objeto RoleManager<T> (use IdentityRole para el tipo predeterminado) para enumerar y administrar los roles.

Nota: En los proyectos de Blazor WebAssembly, tendrá que proporcionar API de servidor para realizar estas operaciones (en lugar de usar UserManager<T> o RoleManager<T> directamente). Una aplicación cliente de Blazor WebAssembly administraría las notificaciones o los roles mediante la llamada segura de puntos de conexión de API expuestos para este fin.

Guía de migración

Para la migración desde ASP.NET Web Forms y proveedores universales a ASP.NET Core Identity se necesitan varios pasos:

  1. Crear el esquema de base de datos de ASP.NET Core Identity en la base de datos de destino
  2. Realizar la migración de los datos desde el esquema del proveedor universal al de ASP.NET Core Identity
  3. Realizar la migración de la configuración desde web.config a middleware y servicios, normalmente en Program.cs (o una clase Startup)
  4. Actualizar páginas individuales mediante controles y condicionales para usar aplicaciones auxiliares de etiquetas y nuevas API de identidad.

En las siguientes secciones se describen estos pasos.

Creación del esquema ASP.NET Core Identity

Hay varias maneras de crear la estructura de tablas necesaria que se usa para ASP.NET Core Identity. La más sencilla consiste en crear una aplicación web de ASP.NET Core. Elija Aplicación web y, después, cambie el tipo de autenticación para usar cuentas individuales.

Nuevo proyecto con cuentas individuales

Desde la línea de comandos, puede hacer lo mismo si ejecuta dotnet new webapp -au Individual. Una vez que se ha creado la aplicación, ejecútela y regístrela en el sitio. Debería desencadenar una página como la que se muestra a continuación:

Página de aplicación de migraciones

Haga clic en el botón "Aplicar migraciones" para que se creen de forma automática las tablas de base de datos necesarias. Además, los archivos de migración deben aparecer en el proyecto, como se muestra a continuación:

Archivos de migración

Puede ejecutar la migración personalmente, sin ejecutar la aplicación web, mediante esta herramienta de línea de comandos:

dotnet ef database update

Si prefiere ejecutar un script para aplicar el nuevo esquema a una base de datos existente, puede crear scripts de estas migraciones desde la línea de comandos. Ejecute este comando para generar el script:

dotnet ef migrations script -o auth.sql

El comando anterior generará un script SQL en el archivo de salida auth.sql, que se puede ejecutar en cualquier base de datos que prefiera. Si tiene algún problema al ejecutar los comandos dotnet ef, asegúrese de que tiene las herramientas de EF Core instaladas en el sistema.

En el caso de que tenga columnas adicionales en las tablas de origen, tendrá que identificar la mejor ubicación para estas columnas en el nuevo esquema. Por lo general, las columnas que se encuentran en la tabla aspnet_Membership se deben asignar a la tabla AspNetUsers. Las columnas de aspnet_Roles se deben asignar a AspNetRoles. Cualquier columna adicional de la tabla aspnet_UsersInRoles se agregará a la tabla AspNetUserRoles.

También merece la pena considerar la posibilidad de colocar las columnas adicionales en tablas independientes. De este modo, las migraciones futuras no deberán tener en cuenta estas personalizaciones del esquema de identidad predeterminado.

Migración de datos desde proveedores universales a ASP.NET Core Identity

Una vez que tenga el esquema de tablas de destino, el siguiente paso consiste en realizar la migración de los registros de usuario y de rol al nuevo esquema. Aquí puede encontrar una lista completa de las diferencias de esquema, incluidas qué columnas se asignan a las columnas nuevas.

Para realizar la migración de los usuarios de la pertenencia a las nuevas tablas de identidad, debe seguir los pasos descritos en la documentación. Después de seguir estos pasos y el script proporcionado, los usuarios tendrán que cambiar su contraseña la próxima vez que inicien sesión.

Se puede realizar la migración de las contraseñas de usuario, pero el proceso es mucho más complicado. Exigir a los usuarios que actualicen sus contraseñas como parte del proceso de migración y animarles a usar contraseñas nuevas y únicas, es probable que mejore la seguridad global de la aplicación.

Migración de la configuración de seguridad de web.config a inicio de la aplicación

Como se ha indicado antes, los proveedores de roles y la pertenencia a ASP.NET se configuran en el archivo web.config de la aplicación. Como las aplicaciones de ASP.NET Core no están asociadas a IIS y usan un sistema independiente para la configuración, esta configuración se debe establecer en otra parte. En su mayor parte, ASP.NET Core Identity se configura en el archivo Program.cs. Abra el proyecto web que se ha creado antes (para generar el esquema de la tabla de identidad) y revise el archivo Program.cs (o Startup.cs).

Este código agrega compatibilidad con EF Core e Identity:

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
    options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

El método de extensión AddDefaultIdentity se usa para configurar Identity con el fin de utilizar el objeto ApplicationDbContext predeterminado y el tipo IdentityUser del marco. Si usa un objeto IdentityUser personalizado, asegúrese de especificar aquí su tipo. Si estos métodos de extensión no funcionan en la aplicación, compruebe que tiene las directivas using adecuadas y las referencias de paquetes NuGet necesarias. Por ejemplo, en el proyecto se debe hace referencia a alguna versión de los paquetes Microsoft.AspNetCore.Identity.EntityFrameworkCore y Microsoft.AspNetCore.Identity.UI.

También en Program.cs, debería ver el middleware necesario configurado para el sitio. En concreto, se deben configurar UseAuthentication y UseAuthorization, y en la ubicación adecuada.

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

//app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

ASP.NET Identity no configura el acceso anónimo o basado en roles a ubicaciones desde Program.cs. Tendrá que realizar la migración de los datos de configuración de autorización específicos de la ubicación a filtros de ASP.NET Core. Anote las carpetas y páginas en las que se necesitarán estas actualizaciones. Realizará estos cambios en la sección siguiente.

Actualización de páginas individuales para usar abstracciones de ASP.NET Core Identity

En la aplicación de ASP.NET Web Forms, si tiene valores web.config para denegar a los usuarios anónimos el acceso a determinadas páginas o carpetas, debe realizar la migración de estos cambios mediante la adición del atributo [Authorize] a esas páginas:

@attribute [Authorize]

Si ha denegado el acceso todavía más excepto para los usuarios que pertenecen a un rol concreto, tendría que migrar igualmente este comportamiento mediante la adición de un atributo que especifique un rol:

@attribute [Authorize(Roles ="administrators")]

El atributo [Authorize] solo funciona en los componentes @page a los que se accede por medio del enrutador de Blazor. El atributo no funciona con componentes secundarios que, en su lugar, deben usar AuthorizeView.

Si tiene lógica en el marcado de página para determinar si se va a mostrar código a un usuario concreto, puede reemplazarla por el componente AuthorizeView. El componente AuthorizeView muestra selectivamente la interfaz de usuario en función de si el usuario está autorizado para verla. También expone una variable context que se puede usar para acceder a información del usuario.

<AuthorizeView>
    <Authorized>
        <h1>Hello, @context.User.Identity.Name!</h1>
        <p>You can only see this content if you are authenticated.</p>
    </Authorized>
    <NotAuthorized>
        <h1>Authentication Failure!</h1>
        <p>You are not signed in.</p>
    </NotAuthorized>
</AuthorizeView>

Puede acceder al estado de autenticación dentro de la lógica de procedimientos si accede al usuario desde un elemento Task<AuthenticationState configurado con el atributo [CascadingParameter]. Esta configuración le permitirá acceder al usuario, lo que le permite determinar si está autenticado y si pertenece a un rol concreto. Si tiene evaluar una directiva mediante procedimientos, puede insertar una instancia de IAuthorizationService y, dentro, llamadas al método AuthorizeAsync. En el código de ejemplo siguiente se muestra cómo obtener información del usuario y permitir que un usuario autorizado realice una tarea restringida por la directiva content-editor.

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

<button @onclick="@DoSomething">Do something important</button>

@code {
    [CascadingParameter]
    private Task<AuthenticationState> authenticationStateTask { get; set; }

    private async Task DoSomething()
    {
        var user = (await authenticationStateTask).User;

        if (user.Identity.IsAuthenticated)
        {
            // Perform an action only available to authenticated (signed-in) users.
        }

        if (user.IsInRole("admin"))
        {
            // Perform an action only available to users in the 'admin' role.
        }

        if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
            .Succeeded)
        {
            // Perform an action only available to users satisfying the
            // 'content-editor' policy.
        }
    }
}

El primer elemento AuthenticationState se debe configurar como un valor en cascada antes de poder enlazarlo a un parámetro en cascada como este. Esto se suele hacer mediante el componente CascadingAuthenticationState. Normalmente esta configuración se realiza en App.razor:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Resumen

Blazor usa el mismo modelo de seguridad que ASP.NET Core, que es ASP.NET Core Identity. La migración desde proveedores universales a ASP.NET Core Identity es relativamente sencilla, siempre que no se haya aplicado demasiada personalización al esquema de datos original. Una vez que se han migrado los datos, el trabajo con la autenticación y la autorización en las aplicaciones Blazor está bien documentado, con compatibilidad configurable y mediante programación con la mayoría de los requisitos de seguridad.

Referencias