Segurança: autenticação e autorização no ASP.NET Web Forms e no Blazor

Dica

Esse conteúdo é um trecho do livro eletrônico, Blazor para Desenvolvedores do ASP NET Web Forms para o Azure, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

Blazor-for-ASP-NET-Web-Forms-Developers eBook cover thumbnail.

Migrar de um aplicativo ASP.NET Web Forms para o Blazor certamente exigirá a atualização de como a autenticação e a autorização são executadas, supondo que o aplicativo tenha autenticação configurada. Este capítulo abrange como migrar do modelo de provedor universal do ASP.NET Web Forms (para associação, funções e perfis de usuário) e como trabalhar com o ASP.NET Core Identity em aplicativos Blazor. Embora este capítulo abranja etapas e considerações gerais, as etapas e os scripts detalhados podem ser encontrados na documentação referenciada.

Provedores universais ASP.NET

Desde o ASP.NET 2.0, a plataforma ASP.NET Web Forms dá suporte a um modelo de provedor para uma variedade de recursos, incluindo associação. O provedor de associação universal, juntamente com o provedor de função opcional, normalmente é implantado com aplicativos ASP.NET Web Forms. Ele oferece uma maneira robusta e segura de gerenciar a autenticação e a autorização que continua a funcionar bem até hoje. A oferta mais recente desses provedores universais está disponível como um pacote NuGet, Microsoft.AspNet.Providers.

Os Provedores Universais funcionam com um esquema de banco de dados SQL que inclui tabelas como aspnet_Applications, aspnet_Membership, aspnet_Roles e aspnet_Users. Quando configurados pela execução do comando aspnet_regsql.exe, os provedores instalam tabelas e procedimentos armazenados que fornecem todas as consultas e comandos necessários para trabalhar com os dados subjacentes. O esquema de banco de dados e esses procedimentos armazenados não são compatíveis com sistemas ASP.NET Identity e ASP.NET Core Identity mais recentes e, portanto, os dados existentes precisam ser migrados para o novo sistema. A Figura 1 mostra um esquema de tabela de exemplo configurado para provedores universais.

universal providers schema

O provedor universal lida com usuários, associação, funções e perfis. Os usuários são atribuídos a identificadores globalmente exclusivos e as informações básicas, como userId, userName etc., são armazenadas na tabela aspnet_Users. As informações de autenticação, como senha, formato de senha, sal de senha, contadores de bloqueio e detalhes, etc., são armazenadas na tabela aspnet_Membership. As funções consistem simplesmente em nomes e identificadores exclusivos, que são atribuídos aos usuários por meio da tabela de associação aspnet_UsersInRoles, fornecendo uma relação muitos para muitos.

Se o sistema existente estiver usando funções além da associação, você precisará migrar as contas de usuário, as senhas associadas, as funções e a associação de função ao ASP.NET Core Identity. Você também provavelmente precisará atualizar seu código em que está executando verificações de função com instruções if para, em vez disso, aproveitar filtros declarativos, atributos e/ou auxiliares de marca. Revisaremos as considerações de migração em mais detalhes no final deste capítulo.

Configuração de autorização no Web Forms

Para configurar o acesso autorizado a determinadas páginas em um aplicativo ASP.NET Web Forms, normalmente você especifica que determinadas páginas ou pastas ficam inacessíveis para usuários anônimos. Essa configuração é feita no arquivo 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>

A seção de configuração authentication define a autenticação de formulários para o aplicativo. A seção authorization é usada para não permitir usuários anônimos em todo o aplicativo. No entanto, você pode fornecer regras de autorização mais granulares por local e também aplicar verificações de autorização baseadas em função.

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

A configuração acima, quando combinada com a primeira, permitiria que usuários anônimos acessassem a página de logon, substituindo a restrição em todo o site para usuários não autenticados.

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

A configuração acima, quando combinada com as outras, restringe o acesso à pasta /admin e a todos os recursos dentro dela a membros da função "Administradores". Essa restrição também pode ser aplicada com a colocação de um arquivo web.config separado dentro da raiz da pasta /admin.

Código de autorização no Web Forms

Além de configurar o acesso usando o web.config, você também poderá configurar programaticamente o acesso e o comportamento em seu aplicativo Web Forms. Por exemplo, você pode restringir a capacidade de executar determinadas operações ou exibir determinados dados com base na função do usuário.

Esse código pode ser usado tanto na lógica code-behind quanto na própria página:

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

Além de verificar a associação de função de usuário, você também pode determinar se elas são autenticadas (embora geralmente isso seja feito melhor com o uso da configuração baseada em localização abordada acima). Veja abaixo um exemplo dessa abordagem.

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

No código acima, o RBAC (controle de acesso baseado em função) é usado para determinar se determinados elementos da página, como um SecretPanel, são visíveis com base na função do usuário atual.

Normalmente, os aplicativos ASP.NET Web Forms configuram a segurança dentro do arquivo web.config e adicionam mais verificações quando necessário em páginas .aspx e em seus arquivos code-behind .aspx.cs relacionados. A maioria dos aplicativos aproveita o provedor de associação universal, frequentemente com o provedor de função adicional.

Identidade do ASP.NET Core

Embora ainda tenha a tarefa de autenticação e autorização, a Identidade do ASP.NET Core usa um conjunto diferente de abstrações e suposições quando comparado aos provedores universais. Por exemplo, o novo modelo de identidade dá suporte à autenticação de terceiros, permitindo que os usuários se autentiquem usando uma conta de mídia social ou outro provedor de autenticação confiável. A Identidade do ASP.NET Core dá suporte à interface do usuário para páginas normalmente necessárias, como logon, logoff e registro. Ele aproveita o EF Core para seu acesso a dados e usa as migrações do EF Core para gerar o esquema necessário a fim de dar suporte ao modelo de dados. Esta introdução à Identidade do ASP.NET Core fornece uma boa visão geral do que está incluído na Identidade do ASP.NET Core e como começar a trabalhar com ele. Se você ainda não tiver a Identidade do ASP.NET Core em seu aplicativo e em seu banco de dados, isso ajudará você a começar.

Funções, declarações e políticas

Os provedores universais e a Identidade do ASP.NET Core suportam o conceito de funções. Você pode criar funções para usuários e atribuir usuários a funções. Os usuários podem pertencer a várias funções, e você pode verificar a associação de função como parte de sua implementação de autorização.

Além das funções, o ASP.NET Core dá suporte aos conceitos de declarações e políticas. Embora uma função deva corresponder especificamente a um conjunto de recursos que um usuário nessa função deve ser capaz de acessar, uma declaração é simplesmente parte da identidade de um usuário. Uma declaração é um par de valores de nome que representa o titular, não o que o titular pode fazer.

É possível inspecionar diretamente as declarações de um usuário e determinar com base nesses valores se um usuário deve receber acesso a um recurso. No entanto, essas verificações geralmente são repetitivas e dispersas em todo o sistema. Uma abordagem melhor é definir uma política.

Uma política de autorização consiste em um ou mais requisitos. As políticas são registradas como parte da configuração do serviço de autorização no método ConfigureServices de Startup.cs. Por exemplo, o snippet de código a seguir configura uma política chamada "CanadiansOnly", cujo requisito é o de que o usuário tenha a declaração Country com o valor "Canadá".

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

Você pode saber mais sobre como criar políticas personalizadas na documentação.

Se você estiver usando políticas ou funções, poderá especificar que uma página específica em seu aplicativo Blazorexija essa função ou política com o atributo [Authorize], aplicado com a diretiva @attribute.

Exigindo uma função:

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

Exigir que uma política seja atendida:

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

Se você precisar acessar o estado de autenticação, as funções ou declarações de um usuário em seu código, há duas maneiras principais de obter essa funcionalidade. A primeira é receber o estado de autenticação como um parâmetro em cascata. A segunda é acessar o estado usando um AuthenticationStateProvider injetado. Os detalhes de cada uma dessas abordagens estão descritos na BlazorDocumentação de segurança.

O código abaixo mostra como receber o AuthenticationState como um parâmetro em cascata:

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

Com esse parâmetro, você pode obter o usuário usando este código:

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

O código abaixo mostra como injetar o AuthenticationStateProvider:

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

Com o provedor, você pode obter acesso ao usuário com o seguinte código:

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

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

Observação: o componente AuthorizeView, abordado posteriormente neste capítulo, fornece uma maneira declarativa de controlar o que um usuário vê em uma página ou componente.

Para trabalhar com usuários e declarações (em aplicativos de servidor do Blazor), talvez você também precise injetar um UserManager<T> (use IdentityUser por padrão), que pode ser usado para enumerar e modificar declarações de um usuário. Primeiro, insira o tipo e atribua-o a uma propriedade:

@inject UserManager<IdentityUser> MyUserManager

Em seguida, use-o para trabalhar com as declarações do usuário. O exemplo abaixo mostra como adicionar e persistir uma declaração em um usuário:

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

Se você precisar trabalhar com funções, siga a mesma abordagem. Talvez seja necessário injetar um RoleManager<T> (use IdentityRole para o tipo padrão) a fim de listar e gerenciar as próprias funções.

Observação: em projetos WebAssembly do Blazor, você precisará fornecer APIs de servidor para executar essas operações (em vez de usar UserManager<T> ou RoleManager<T> diretamente). Um aplicativo cliente WebAssembly do Blazor gerenciaria declarações e/ou funções chamando com segurança os pontos de extremidade de API expostos para essa finalidade.

Guia de migração

A migração de provedores ASP.NET Web Forms e universais para a Identidade do ASP.NET Core requer várias etapas:

  1. Criar esquema de banco de dados da Identidade do ASP.NET no banco de dados de destino
  2. Migrar dados do esquema de provedor universal para o esquema da Identidade do ASP.NET Core
  3. Migre a configuração do web.config para middleware e serviços, normalmente em Program.cs (ou uma classe Startup)
  4. Atualize páginas individuais com controles e condicionais para usar auxiliares de marca e novas APIs de identidade.

Cada uma dessas etapas é descrita mais detalhadamente nas seções a seguir.

Criando o esquema da Identidade do ASP.NET Core

Há várias maneiras de criar a estrutura de tabela necessária usada para a Identidade do ASP.NET Core. A mais simples é criar um novo aplicativo Web ASP.NET Core. Escolha Aplicativo Web e altere o Tipo de Autenticação para usar Contas Individuais.

new project with individual accounts

Na linha de comando, você pode fazer a mesma coisa executando dotnet new webapp -au Individual. Depois que o aplicativo tiver sido criado, execute-o e registre-o no site. Você deve disparar uma página como a mostrada abaixo:

apply migrations page

Clique no botão "Aplicar Migrações"; as tabelas de banco de dados necessárias devem ser criadas para você. Além disso, os arquivos de migração devem aparecer em seu projeto, conforme mostrado:

migration files

Você pode executar a migração por você mesmo, sem executar o aplicativo Web, com esta ferramenta de linha de comando:

dotnet ef database update

Se preferir executar um script para aplicar o novo esquema a um banco de dados existente, você poderá executar o script dessas migrações na linha de comando. Execute este comando para gerar o script:

dotnet ef migrations script -o auth.sql

O comando acima produzirá um script SQL no arquivo de saída auth.sql, que pode ser executado em qualquer banco de dados que desejar. Se você tiver problemas para executar comandos dotnet ef, tenha as ferramentas EF Core instaladas em seu sistema.

Caso você tenha colunas adicionais em suas tabelas de origem, precisará identificar o melhor local para essas colunas no novo esquema. Em geral, as colunas encontradas na tabela aspnet_Membership devem ser mapeadas para a tabela AspNetUsers. As colunas em aspnet_Roles devem ser mapeadas para AspNetRoles. Todas as colunas adicionais na tabela aspnet_UsersInRoles seriam adicionadas à tabela AspNetUserRoles.

Também vale a pena considerar colocar colunas adicionais em tabelas separadas. Para que as migrações futuras não precisem levar em conta essas personalizações do esquema de identidade padrão.

Migrando dados de provedores universais para a Identidade do ASP.NET Core

Quando você tiver o esquema da tabela de destino, a próxima etapa será migrar seus registros de usuário e função para o novo esquema. Uma lista completa das diferenças de esquema, incluindo quais colunas são mapeadas para quais novas colunas, pode ser encontrada aqui.

Para migrar os usuários da associação para as novas tabelas de identidade, você deve seguir as etapas descritas na documentação. Depois de seguir essas etapas e o script fornecido, os usuários precisarão alterar sua senha na próxima vez em que fizerem logon.

É possível migrar senhas de usuário, mas o processo é muito mais complexo. Exigir que os usuários atualizem suas senhas como parte do processo de migração e incentivá-los a usar senhas novas e exclusivas provavelmente aumentará a segurança geral do aplicativo.

Migrando configurações de segurança de web.config para a inicialização do aplicativo

Conforme observado acima, a associação ASP.NET e os provedores de função são configurados no arquivo web.config do aplicativo. Como os aplicativos ASP.NET Core não estão vinculados ao IIS e usam um sistema separado para configuração, essas configurações devem ser definidas em outro lugar. Na sua maioria, a Identidade do ASP.NET Core é configurada no arquivo Program.cs. Abra o projeto Web que foi criado anteriormente (para gerar o esquema da tabela de identidade) e examine o arquivo Program.cs (ou Startup.cs).

Este código adiciona suporte ao EF Core e à Identidade:

// 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>();

O método de extensão AddDefaultIdentity é usado para configurar a identidade a fim de usar o padrão ApplicationDbContext e o tipo IdentityUser da estrutura. Se você estiver usando um IdentityUser personalizado, especifique seu tipo aqui. Se esses métodos de extensão não estiverem funcionando em seu aplicativo, verifique se você tem as instruções apropriadas de uso e se tem as referências necessárias do pacote NuGet. Por exemplo, seu projeto deve ter alguma versão dos pacotes Microsoft.AspNetCore.Identity.EntityFrameworkCore e Microsoft.AspNetCore.Identity.UI referenciados.

Também em Program.cs, você deve ver o middleware necessário configurado para o site. Especificamente, UseAuthentication e UseAuthorization devem ser configurados e devem estar no local apropriado.

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

A Identidade do ASP.NET não configura o acesso anônimo ou baseado em função a locais do Program.cs. Você precisará migrar os dados de configuração de autorização específicos do local para filtros no ASP.NET Core. Anote quais pastas e páginas exigirão essas atualizações. Você fará essas alterações na próxima seção.

Atualizando páginas individuais para usar abstrações da Identidade do ASP.NET Core

Em seu aplicativo ASP.NET Web Forms, se você tivesse configurações web.config para negar acesso a determinadas páginas ou pastas a usuários anônimos, migraria essas alterações adicionando o atributo [Authorize] a tais páginas:

@attribute [Authorize]

Se você tivesse o acesso negado, exceto para usuários que pertençam a determinada função, migraria o comportamento com a adição de um atributo especificando uma função:

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

O atributo [Authorize] só funciona em componentes @page que são alcançados por meio do Roteador do Blazor. O atributo não funciona com componentes filhos, que devem usar AuthorizeView.

Se você tiver lógica dentro da marcação de página para determinar se um código deve ser exibido para determinado usuário, poderá substituí-lo pelo componente AuthorizeView. O componente AuthorizeView exibe de forma seletiva a interface do usuário, caso o usuário esteja autorizado a vê-la. Ele também expõe uma variável context que pode ser usada para acessar informações do usuário.

<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>

Você pode acessar o estado de autenticação na lógica de procedimento acessando o usuário de um Task<AuthenticationState configurado com o atributo [CascadingParameter]. Essa configuração dará acesso ao usuário, que pode permitir que você determine se eles são autenticados e se pertencem a uma função específica. Se você precisar avaliar uma política com um procedimento, poderá injetar uma instância do IAuthorizationService e chamar o método AuthorizeAsync nele. O código de exemplo a seguir demonstra como obter informações do usuário e permitir que um usuário autorizado execute uma tarefa restrita pela política 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.
        }
    }
}

A AuthenticationState primeiro precisa ser configurada como um valor em cascata antes que possa ser associada a um parâmetro em cascata como esse. Isso normalmente é feito usando o componente CascadingAuthenticationState. Essa configuração normalmente é feita em 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>

Resumo

Blazor usa o mesmo modelo de segurança que o ASP.NET Core, que é a Identidade do ASP.NET Core. Migrar de provedores universais para a Identidade do ASP.NET Core é relativamente simples, supondo que não tenha sido aplicada muita personalização ao esquema de dados original. Depois que os dados são migrados, o trabalho com autenticação e autorização em aplicativos Blazor está bem documentado, com suporte configurável e de programação para a maioria dos requisitos de segurança.

Referências