Autenticação e Autorização no ASP.NET MVC

by Israel Aece 9. February 2010 08:58

Desde a primeira versão do ASP.NET, temos três formas de autenticação: Windows, Forms e Passport. A primeira opção consiste em permitir o acesso desde que o usuário faça parte do domínio/máquina. Essa opção torna o gerenciamento simples, facilita o Single Sign-On (SSO) interno, mas é útil em um ambiente controlado, como uma intranet. A opção Forms faz com que a aplicação se encarregue de gerenciar e autenticar seus usuários, baseando-se em um cookie que determinará quem é o usuário logado, e esta opção é a mais ideal para a internet. Finalmente, temos a Passport, que permitia autenticar o usuário utilizando o serviço de identidade da Microsoft, mas por haver um custo envolvido, acabou não evoluindo.

Com a vinda do ASP.NET MVC, grande parte dos serviços que são disponibilizados para o ASP.NET WebForms, já nasceram com o MVC ou estão aos poucos sendo adicionados à plataforma. Como já era de se esperar, a questão da autenticação e autorização já fazem parte desde a versão 1.0 do MVC, onde podemos utilizar a autenticação Windows ou Forms, mudando ligeiramente como devemos proceder para configurá-los e utilizá-los.

Para definir se vamos utilizar o modelo de autenticação Windows ou Forms, devemos recorrer ao elemento authentication no arquivo Web.config, como já fazíamos anteriormente. O atributo mode determina qual a opção de autenticação, e um sub-elemento chamado forms, nos permite customizar a autenticação baseada em Forms. O exemplo abaixo ilustra a configuração inicial:

<authentication mode="Forms">
    <forms loginUrl="Authentication/LogOn" />
</authentication>

Aqui há uma mudança radical aqui. O ASP.NET WebForms é baseado em arquivos, ou seja, todas as requisições, sejam elas da aplicação ou da infraestrutura (como é o caso aqui), o alvo sempre será um arquivo físico, com extensão *.aspx. Já no ASP.NET MVC, o alvo será sempre uma ação, que nada mais é que um método dentro de uma classe, conhecida como Controller. No exemplo acima, o controller é uma classe chamada AuthenticationController e dentro dele há um método chamado LogOn. É dentro deste método que você colocará todos os passos necessários para autenticar o usuário. Geralmente esse método deve receber o login e a senha, mas eventualmente poderá ter algo mais rebuscado.

Quando tentamos acessar um recurso protegido através do FormsAuthentication, ele automaticamente redireciona o usuário para a página de autenticação configurada acima, embutindo na URL uma query string chamada ReturnUrl, com o caminho (do arquivo ou da ação) que tentamos acessar. Esse parâmetro é útil para depois que validar o usuário, redirecioná-lo para a mesma seção que ele solicitou antes de efetuar a autenticação. Sendo assim, é necessário que o método que efetua a autenticação do usuário, também receba esse parâmetro.

Em tempo, a versão 2.0 do ASP.NET MVC fornece um atributo chamado HttpPostAttribute, que permite você decorar uma determinada ação, para que ela seja executada somente via POST, garantindo assim que ela seja executada somente através desta forma. Para ilustrar, o método LogOn fica da seguinte forma:

public class AuthenticationController : Controller
{
    [HttpPost]
    public ActionResult LogOn(string userName, string password, string returnUrl)
    {
        //...
    }
}

É importante dizer que se você estiver utilizando a autenticação Windows, nada disso é necessário, já que a autenticação acaba sendo feita automaticamente pelo IIS/ASP.NET.

Se precisar saber qual usuário está autenticado naquele momento, você pode recorrer à propriedade User, exposta pela classe Controller. Assim como já acontece com a classe Page do WebForms, essa propriedade retorna a instância de uma classe que implementa a interface IPrincipal, fornecendo acesso ao username do usuário atual, e um método chamado IsInRole, caso você precisa refinar ainda mais a autorização. Para maiores detalhes sobre essa interface, consulte este o capítulo 9 deste artigo.

Autorização

A autorização pode continuar sendo realizada da mesma forma que fazíamos antes, ou seja, recorrendo ao arquivo de configuração (Web.config) para especificar as regras de autorização. A diferença se dá também pelo fato de que o MVC é baseado em ações, e ao invés de determinar páginas e/ou diretórios nessas regras, utilizaremos as ações para refinar o acesso dos usuários. Se temos um controller chamado MonitorController e dentro dele uma ação (método) chamada VisualizarItens, e o acesso à ela somente pudesse ser feita por usuários autenticados, a regra ficaria da seguinte forma:

<location path="Monitor/VisualizarItens">
    <system.web>
        <authorization>
            <deny users="?" />
        </authorization>
    </system.web>
</location>

Caso você precise permitir o acesso à uma determinada ação para somente aqueles usuários que fazem parte de um determinado grupo (role), você pode configurar a regra da seguinte forma:

<location path="Monitor/VisualizarItens">
    <system.web>
        <authorization>
            <allow roles="Financeiro" />
        </authorization>
    </system.web>
</location>

Apesar desta técnica funcionar, é difícil de manter. Para cada novo controller ou ação que são criados, você precisa se preocupar em olhar para o arquivo de configuração e configurar quem poderá ou não ter acesso. O problema desta técnica é que se você esquecer, usuários que talvez não deveriam ter acesso, acabarão visualizando. No ASP.NET MVC há uma outra forma de se trabalhar, também declarativa, mas recorrendo à atributos dentro dos controllers/ações, facilitando a centralização e manutenção das regras de acesso.

Para isso, o ASP.NET MVC fornece um atributo chamado AuthorizeAttribute que podemos decorar em classes (controllers) ou em métodos (ações). Esse atributo fornece duas propriedades do tipo string: Users e Roles. Na primeira propriedade, especificamos os usuários (separados por vírgula) que podem ter acesso aquele membro (classe ou método) em que o atributo está sendo aplicado. Já a segunda propriedade, Roles, permite especificarmos os grupos (separados por vírgula), onde somente usuários que estão dentro de um deles é que podem ter acesso ao recurso. Utilizar a propriedade Users não é o mais ideal, porque usuários são muito "voláteis", e em pouco tempo, essa regra provavelmente não fará mais sentido.

Como dito acima, podemos aplicar esse atributo em nível de controller, e assim, as suas respectivas ações irão exigir que aquela regra seja atendida antes de permitir o acesso. No primeiro exemplo, aplicamos este atributo em nível de controller, e todas as ações exigirão que o usuário esteja autenticado:

[Authorize]
public class MonitorController : Controller
{
    public ActionResult VisualizarItens()
    {
        //...
    }

    public ActionResult RecuperarIndice()
    {
        //...
    }
}

Já o segundo exemplo, fará com que o controller seja acessado somente por usuários devidamente autenticados, mas isso não é o suficiente para o método VisualizarItens, que além disso, exige que o usuário esteja contido no grupo Financeiro:

[Authorize]
public class MonitorController : Controller
{
    [Authorize(Roles = "Financeiro")]
    public ActionResult VisualizarItens()
    {
        //...
    }

    public ActionResult RecuperarIndice()
    {
        //...
    }
}

Caso você tenha uma regra de validação muito mais complexa do que isso, nada impede você de criar um filtro para efetuar essa customização, fazendo que a autorização siga os passos necessários para atender essa regra predefinida. E para finalizar, lembre-se que o MVC trata os métodos como "opt-out", ou seja, por padrão, todos os métodos públicos estão acessíveis. Se mesmo com a autenticação e/ou autorização ele jamais deverá ser invocado, então você deve decorá-lo com o atributo NonActionAttribute.

Membership e Roles

A partir da versão 2.0 do ASP.NET, uma série de novos serviços foram adicionados, e entre eles temos o Memberhip Provider e Role Provider, algo que já falei extensivamente nestes outros artigos. Felizmente podemos continuar utilizando esses serviços, mas com um pouco mais de cautela.

Esses serviços seguem um padrão conhecido como Provider Model, e ao utilizar o Membership ou Roles, eu estou trabalhando de forma independente de repositório; posso trabalhar com SQL Server, Oracle ou Xml, apenas configurando os providers através do arquivo Web.config. Mas apesar desses serviços seguirem um padrão de extensibilidade bem definido, isso não quer dizer que é uma boa prática você pode utilizá-los diretamente dentro dos controllers.

É importante dizer que ao criar aplicações MVC, a testabilidade deve estar em foco, e utilizar as classes Membership ou Roles dentro dos controllers, o tornará dependente destas implementações, ou melhor, destes serviços, e que por sua vez, estão fortemente acoplados ao ASP.NET. Como disse anteriormente, a arquitetura do provider model é bem flexível, mas para criar uma versão "fake" é complexo demais, pois há uma infinidade de funcinalidades que as vezes serão desnecessárias. Além disso, utilizar essas classes torna meu controller dependente deste tipo de serviço mas, eventualmente, eu tenho o meu próprio repositório de usuários e suas respectivas permissões, e quero fazer uso deles ao invés daqueles que o ASP.NET fornece.

A classe AccountController

Quando criamos um novo projeto MVC, por padrão, uma série de recursos já são adicionados, e entre eles, temos a classe (controller) chamada AccountController. Essa classe foi desenhada para servir como um wrapper para os serviços de Membership e FormsAuthentication. Há vários problemas com essa classe, e o primeiro deles é aquele que descrevi acima, que é a forte dependência da API do Membership. Apesar da Microsoft ter refatorado o código e criado uma interface chamada IMembershipService, há ainda alguns tipos que estão sendo utilizados, como é o caso do enumerador MembershipCreateStatus. Além disso, essa classe ainda viola o princípio de SRP (Single Responsability Principle), que faz com que ela faça muito mais coisas do que realmente deveria.

Conclusão: O fato da Microsoft tem se preocupado em manter grande parte dos recursos do WebForms no MVC, é importante que você analise cuidadosamente como incorporá-lo ao MVC para não comprometer um dos maiores benefícios do MVC, que são os testes. É importante notar que grande parte do conhecimento adquirido no ASP.NET WebForms, acaba sendo reutilizado aqui, apenas com ligeiras mudanças devido à arquitetura do MVC.

Tags: , , ,

ASP.NET | Security

Compatibilidade dos Providers

by Israel Aece 9. November 2009 05:28

Quando utilizamos qualquer uma das funcionalidades que são expostas pelo ASP.NET (membership, roles, profile, webparts ou health monitoring), geralmente recorremos ao SQL Server para armazenar essas informações. E para isso, tudo o que precisamos fazer é executar o utilitário aspnet_regsql.exe, que nos permite informar uma base de dados e as funcionalidades que desejamos instalar.

Uma vez instalado, fazemos todo o desenvolvimento em cima deste banco, e quando chega o momento de instalar a aplicação no servidor, muitas vezes um script com a estrutura da base de dados é gerado, e mais tarde, executado neste servidor que hospedará a aplicação. O problema é que o script somente levará a estrutura e não os dados. Com isso, ao executar a aplicação a partir do servidor, teremos a seguinte mensagem sendo disparada:

The 'System.Web.Security.SqlMembershipProvider' requires a database schema compatible with schema version '1'.  However, the current database schema is not compatible with this version.  You may need to either install a compatible schema with aspnet_regsql.exe (available in the framework installation directory), or upgrade the provider to a newer version.

Entre as tabelas que o utilitário cria para suportar as funcionalidades, temos a tabela aspnet_SchemaVersions. Essa tabela armazena a versão atual que está instalada, e é uma informação obrigatória, que o runtime do ASP.NET utiliza, e quando ela não estiver presente, o ASP.NET não conseguirá determinar qual versão é e, consequentemente, não rodará, disparando a mensagem que vimos acima.

Uma das opções que temos para resolver isso, é gerar um script com cláusulas INSERT INTO, criando um registro para cada funcionalidade habilitada, assim como podemos notar no exemplo abaixo:

INSERT INTO [dbo].[aspnet_SchemaVersions] ([Feature], [CompatibleSchemaVersion], [IsCurrentVersion]) 
    VALUES (N'common', N'1', 1)

INSERT INTO [dbo].[aspnet_SchemaVersions] ([Feature], [CompatibleSchemaVersion], [IsCurrentVersion]) 
    VALUES (N'health monitoring', N'1', 1)

INSERT INTO [dbo].[aspnet_SchemaVersions] ([Feature], [CompatibleSchemaVersion], [IsCurrentVersion]) 
    VALUES (N'membership', N'1', 1)

INSERT INTO [dbo].[aspnet_SchemaVersions] ([Feature], [CompatibleSchemaVersion], [IsCurrentVersion]) 
    VALUES (N'personalization', N'1', 1)

INSERT INTO [dbo].[aspnet_SchemaVersions] ([Feature], [CompatibleSchemaVersion], [IsCurrentVersion]) 
    VALUES (N'profile', N'1', 1)

INSERT INTO [dbo].[aspnet_SchemaVersions] ([Feature], [CompatibleSchemaVersion], [IsCurrentVersion]) 
    VALUES (N'role manager', N'1', 1)

Mas, o ideal é utilizar a opção -exportonly do utilitário aspnet_regsql.exe. Ao rodar este comando, ele também gerará um script SQL para adicionar ou remover as funcionalidades (e não executará), mas incluindo tudo o que é necessário, inclusive as respectivas inserções na tabela acima mencionada. De posse deste script, tudo que tem a fazer é rodar no servidor de destino.

Tags:

ASP.NET

Criando novos Providers

by Israel Aece 11. September 2008 13:12

A arquitetura chamada pela Microsoft de Provider Model foi introduzida na versão 2.0 do ASP.NET com a finalidade de podermos configurar um determinado repositório de dados, tornando isso plug and play. Há uma porção de recursos no ASP.NET 2.0 que usa essa técnica, a saber: Profile, Membership, Roles, WebParts e SiteMaps. Os Providers Models utilizam os padrões Abstract Factory e Factory Method (padrões Criacionais) para garantir a genericidade, ou seja, trabalhamos com uma classe abstrata e, em runtime, o ASP.NET se encarregará de instanciar a classe concreta.

Porém, isso não é novidade. O que poucos sabem é que podemos utilizar essa mesma arquitetura para criar nossos próprios providers, não necessariamente vinculando-os a alguma fonte de dados. A idéia deste artigo é ilustrar como devemos proceder para criar o nosso próprio modelo de providers e, tirar todo o proveito fornecido pela plataforma, ou seja, tornar a mudança entre um provider e outro de forma transparente, sem a necessidade de recompilar a aplicação.

Como cenário utilizaremos o seguinte problema: atualmente há três orgãos (Serasa, SCI e SPC) que, dado um número de CPF, retornará um valor booleano indicando se essa pessoa possui ou não restrições financeiras (ReFin). O primeiro passo é criar a classe abstrata que servirá como base para todas as classes derivadas, ou seja, teremos três tipos de consultas, pois teremos três orgãos diferentes. Essa classe base deverá obrigatoriamente herdar, também de uma outra classe abstrata, chamada ProviderBase que, por sua vez, está contida dentro do namespace System.Configuration.Provider. Essa classe fornece a implementação básica para todo e qualquer provider model.

A nossa classe chamará RefinProvider e possuirá apenas um único método abstrato para atender a nossa necessidade que é consultar um determinado número de CPF. Uma vez criada essa classe, ela deverá ser herdada por todas as classes que efetuarão a consulta. Se temos três orgãos, então teremos três classes derivadas de RefinProvider: SerasaRefinProvider, SPCRefinProvider e SCIRefinProvider. O diagrama abaixo ilustra exatamente a hierarquia dessas classes:

Figura 1 - Hierarquia das classes criadas.

Como podemos notar na imagem acima, o método Consultar da classe RefinProvider é implementado nas classes concretas onde, para cada um dos orgãos, terá uma implementação diferente, pois cada um deles exige uma maneira exclusiva de como proceder a consulta. Essas formas irão mesmo variar, e é exatamente essa a flexibilidade que o Provider Model nos proporciona, ou seja, customizar cada uma dessas classes, e apenas com uma pequena configuração no arquivo Web.Config, determinar qual será utilizada pela aplicação. O código abaixo exibe o código da classe que servirá como base para todos os providers:

using System;
using System.Configuration.Provider;

public abstract class RefinProvider : ProviderBase
{
    public abstract bool Consultar(string cpf);
}

O próximo passo é a criação da seção de configuração que devemos ter no arquivo Web.Config. Essa seção de configuração tem os mesmos princípios do membership, roles, etc., que é a possibilidade de listar todos os possíveis providers e depois decidir qual deles usar. Para a criação desta seção de configuração, precisamos criar uma classe que herde da classe abstrata ConfigurationSection, contida dentro do namespace System.Configuration, que representa uma seção no arquivo de configuração. A nossa seção terá duas propriedades: DefaultProvider e Providers; a primeira delas, do tipo string, receberá o nome do provider escolhido para ser utilizado pela aplicação; já a segunda propriedade é do tipo ProviderSettingsCollection, que representa uma coleção de objetos de configuração. O código abaixo exibe a criação desta classe e, mais tarde ainda neste artigo, veremos a sua utilização no arquivo Web.Config.

using System;
using System.Configuration;
using System.Configuration.Provider;

public class RefinSection : ConfigurationSection
{
    [ConfigurationProperty("defaultProvider")]
    public string DefaultProvider
    {
        get
        {
            return (string)base["defaultProvider"];
        }
        set
        {
            base["defaultProvider"] = value;
        }
    }

    [ConfigurationProperty("providers")]
    public ProviderSettingsCollection Providers
    {
        get
        {
            return (ProviderSettingsCollection)base["providers"];
        }
    }
}

Nota: Se alguém quiser saber um pouco mais sobre como proceder para criar uma seção de configuração customizada, consulte este artigo.

Depois da criação das classes, que são os providers, e também da classe que representará a seção de configuração, chega o momento da criação de uma das classes mais importantes, que é uma classe estática que centralizará todo o trabalho, ou seja, extrairá as informações do arquivo de configuração, criará os providers especificados e deixará a disposição da aplicação o provider padrão, ou melhor, aquele que será escolhido para que a aplicação possa usá-lo.

Essa classe receberá o nome de Refin e terá duas propriedades públicas e estáticas chamadas Provider e Providers. A primeira delas, Provider, retornará a instância do provider (RefinProvider) que está atualmente selecionado; já a segunda, Providers, retorná uma coleção do tipo ProviderCollection, contendo todos os providers que foram especificados no arquivo de configuração. Além disso, essa classe ainda possuirá um construtor estático onde, dentro dele, através do método GetSection da classe ConfigurationManager, recuperaremos a instância da seção (RefinSection) que criamos anteriormente.

Ainda dentro do construtor estático, invocamos o método estático InstantiateProviders da classe ProvidersHelper passando para o mesmo as configurações dos providers que deverão ser inicializadas, a instância de uma coleção do tipo ProviderCollection e o tipo dos providers que deverão ser inicializados. Depois que passar pelo método, o segundo parâmetro, que é a coleção, estará devidamente inicializado, ou seja, com todas as instâncias dos providers especificados no arquivo de configuração. Com a coleção definida, então utilizaremos a propriedade DefaultProvider que criamos na classe RefinSection para capturar o provider que a aplicação deve utilizar.

Finalmente, a classe Refin ainda possui, por conveniência, um método estático chamado Consultar que, em seu interior, faz a chamada para o método Consultar da classe concreta, que nada mais é do que o provider que está correntemente selecionado. O código abaixo ilustra todo esse processo que acabamos de ver:

using System;
using System.Configuration;
using System.Configuration.Provider;
using System.Web.Configuration;

public static class Refin
{
    private static readonly RefinProvider _provider;
    private static readonly ProviderCollection _providers;

    public static RefinProvider Provider
    {
        get
        {
            return _provider;
        }
    }

    public static ProviderCollection Providers
    {
        get
        {
            return _providers;
        }
    }

    static Refin()
    {
        RefinSection section =
            (RefinSection)ConfigurationManager.GetSection("refin");

        _providers = new ProviderCollection();
        ProvidersHelper.InstantiateProviders(
            section.Providers, 
            _providers, 
            typeof(RefinProvider));

        string defaultProvider = section.DefaultProvider.Trim();

        if (!string.IsNullOrEmpty(defaultProvider))
            _provider = (RefinProvider)_providers[defaultProvider];
        else
            throw new ConfigurationErrorsException("Provider padrão não definido.");
    }

    public static bool Consultar(string cpf)
    {
        return Refin.Provider.Consultar(cpf);
    }
}

Agora, depois de toda a estrutura de classes definidas, chega o momento de utilizarmos isso na aplicação Web. Como já fizemos a maior parte do código, na aplicação Web resta-nos apenas registrar a seção que criamos anteriormente e fazer o uso dela, adicionando os providers que podemos utilizar e, que neste cenário, estão sendo distribuídos juntamente com o componente que acabamos de desenvolver. O código do arquivo Web.Config fica da seguinte forma:

<?xml version="1.0"?>
<configuration>
    <configSections>
        <section name="refin" type="CSLibrary.RefinSection, CSLibrary" />
    </configSections>
    <refin defaultProvider="SCI">
        <providers>
            <add name="Serasa" type="CSLibrary.SerasaRefinProvider, CSLibrary"/>
            <add name="SPC" type="CSLibrary.SPCRefinProvider, CSLibrary"/>
            <add name="SCI" type="CSLibrary.SCIRefinProvider, CSLibrary"/>
        </providers>
    </refin>
    <system.web>
        <!-- Outras Configurações -->
    </system.web>
</configuration>

Com o código acima, conseguimos assimilar as classes que já criamos e onde elas estão sendo aplicadas neste momento. Registramos a seção RefinSection dentro do elemento configSections para mais adiante fazermos o uso dele. Recapitulando, a classe RefinSection possui duas propriedades: DefaultProvider e Providers. A propriedade DefaultProvider recebe um dos valores definidos no atributo name do elemento add dos providers. O provider que estiver ali selecionado é o que será executado, e ainda podendo ser trocado sem nenhum grande trabalho, pois basta alterar a propriedade DefaultProvider e a recompilação da aplicação não é necessária.

Para finalizar, resta-nos o código na aplicação cliente. Neste momento, utilizaremos o método estático Consultar da classe Refin. Internamente ele invoca o método da classe concreta atualmente selecionada que é exposta através da propriedade Provider. O interessante é que a aplicação nada sabe sobre o provider concreto.

protected void Button1_Click(object sender, EventArgs e)
{
    if (CSLibrary.Refin.Consultar(this.txtCPF.Text))
        Response.Write("Há restrições financeiras para o CPF consultado.");
    else
        Response.Write("Não há restrições financeiras para o CPF consultado.");
}

Como muitos estão familiarizados com as classes de providers fornecidas dentro do .NET Framework, vamos fazer uma comparação entre as classes existentes para Membership e Roles com as classes que criamos no decorrer deste artigo, para assim melhorar a compreensão e também a necessidade de cada uma das classes. Essa comparação é feita através da tabela abaixo:

Refin Membership Roles
RefinSection MembershipSection RoleManagerSection
RefinProvider MembershipProvider RoleProvider
Refin Membership Roles
SerasaRefinProvider, SPCRefinProvider e SCIRefinProvider SqlMembershipProvider e AccessMembershipProvider SqlRoleProvider e AccessRoleProvider

Conclusão: Vimos no decorrer deste artigo que a arquitetura dos Provider Models vai muito além do que a plataforma disponibiliza, que é o caso do Membership, Roles, etc.. Podemos, através dele, tornar as nossas aplicações muito mais flexíveis pois nos permite trocar o provider a qualquer momento, e ainda estender o mesmo, permitindo assim customizar para um novo orgão de consulta (neste cenário) ou para qualquer outra finalidade.

EstendendoProviders.zip (79.71 kb)

Tags: ,

ASP.NET

Integrando Windows Live ID ao ASP.NET

by Israel Aece 27. August 2008 11:11

Como todos sabem, o Windows Live ID é a evolução do Passport. Antigamente, para integrar uma aplicação ASP.NET ao sistema de autenticação do Passport, além de uma SDK que voce precisa entender e customizar, havia custos envolvidos, o que fez com que a terceira forma de autenticação suportada pelo ASP.NET não vingasse.

Atualmente, o Windows Live ID torna-se muito menos complexo e mais simples de acoplar à aplicações ASP.NET e, além disso, não é mais necessário pagar para utilizá-lo. Tudo o que precisa ser feito é um cadastro prévio, que consiste nas informações a respeito da aplicação que fará o uso do Windows Live ID. Para um passo à passo de como configurá-lo, podemos seguir o um artigo que neste endereço.

Além disso, ainda temos (ainda em CTP) o Windows Live Tools, que adiciona alguns controles na barra de ferramentas do Visual Studio .NET. Este CTP contempla, entre vários controles, os controles IDLoginView, IDLoginStatus e a classe LiveMembershipProvider. Esta última, herda diretamente da classe SqlMembershipProvider e traz a possibilidade de integrar uma credencial Live a um usuário dentro da estrutura de segurança do ASP.NET.

Tags: ,

ASP.NET | Security

Instalação padrão do Membership, Roles e Profile

by Israel Aece 27. February 2008 15:18

As funcionalidades Membership, Roles e Profile do ASP.NET 2.0 vem por padrão habilitadas. A questão é que essa configuração padrão demanda ter instalado na máquina onde corre a aplicação (geralmente a máquina do desenvolvedor), o SQL Server Express.

Em uma aplicação recém criada não haverá nenhuma configuração no arquivo Web.Config. Se analisarmos algumas seções que estão presentes do arquivo machine.config, temos:

<connectionStrings>
    <add name="LocalSqlServer" connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true" providerName="System.Data.SqlClient"/>
</connectionStrings>

<membership>
    <providers>
        <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="LocalSqlServer" .... />
    </providers>
</membership>

Com isso, quando iniciamos o ASP.NET Configuration, ele irá tentar acessar (ou criar) esta base de dados. Se não tiver o SQL Server Express instalado, esse processo resultará em um erro. Para resolver, voce tem duas alternativas:

  1. Instalar o SQL Server Express.
  2. Configurar para um servidor SQL Server existente. Se optar por essa alternativa, voce deve configurar o seu arquivo Web.Config da seguinte forma:

<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <clear/>
    <add
      name="SqlConnectionString"
      connectionString="Data Source=local;Initial Catalog=BaseDeDados;Integrated Security=True;"/>
  </connectionStrings>
  <system.web>
    <membership defaultProvider="SqlMembershipProvider">
      <providers>
        <clear/>
        <add
          name="SqlMembershipProvider"
          type="System.Web.Security.SqlMembershipProvider"
          connectionStringName="SqlConnectionString"
          ..... />
      </providers>
    </membership>
  </system.web>
</configuration>

É importante notar que o elemento <clear /> remove a conexão e o provider do membership que são configurados por padrão. Além dessa configuração prévia, voce precisará também criar os objetos (tabelas, stored procedures e views) necessários para que essas funcionalidades trabalhem. Para isso, voce utilizará o utilitário aspnet_regsql.exe e pode encontrar maiores informações aqui.

Tags: ,

ASP.NET | Security

Recuperando Senhas através do Membership

by Israel Aece 12. February 2008 15:26

A API do Membership permite-nos definir qual será o formato da senha que a mesma irá manipular. Voce pode configurar isso através da propriedade passwordFormat no arquivo Web.Config. Esse atributo recebe um dos tres valores abaixo:

  • Clear: As senhas não são criptografadas.
  • Encrypted: Senhas são criptografadas usando as configurações especificadas no elemento de configuração machineKey.
  • Hashed: As senhas são criptografadas utilizando o algoritmo de hash SHA1.

Quando optamos pelo formato Encrypted, há um passo adicional para que isso funcione. Há uma seção no arquivo de configuração chamada machineKey, que é responsável por armazenar informações relacionadas a criptografia de cookies, ViewState, etc.. Em sua configuração padrão, o ASP.NET utiliza chaves geradas automaticamente para aplicar ao algoritmo de criptografia. Esse comportamento impede o funcionamento da extração/definição da senha através do Membership em seu formato Encrypted. Se voce chegar até o método protegido EncryptPassword da classe MembershipProvider, verá que há essa consistencia:

protected virtual byte[] EncryptPassword(byte[] password)
{
    if (MachineKeySection.IsDecryptionKeyAutogenerated)
    {
        throw new ProviderException(SR.GetString("Can_not_use_encrypted_passwords_with_autogen_keys"));
    }
    return MachineKeySection.EncryptOrDecryptData(true, password, null, 0, password.Length);
}

... e isso irá atirar a seguinte exceção:

You must specify a non-autogenerated machine key to store passwords in the encrypted format. Either specify a different passwordFormat, or change the machineKey configuration to use a non-autogenerated decryption key. 

Como voce tem a aplicação sendo executada em uma máquina A e move para uma máquina B, as senhas não serão mais válidas, já que as chaves são diferentes (se for a configuração padrão do ASP.NET). Devido a isso, voce precisará especificar uma chave única para o atributo validationKey e decryptionKey no arquivo Web.Config da aplicação ou no machine.Config caso queira isso para todas as aplicações. Para definir essas chaves, voce poderá utilizar o código abaixo no seu arquivo Web.Config:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <machineKey
      validationKey= "95...23"
      decryptionKey="C7...BD"

      validation="SHA1"
      decryption="AES"/>
  </system.web>
</configuration>

Tags: ,

ASP.NET | Security

Estendendo a classe MembershipUser

by Israel Aece 30. January 2008 15:28

Uma das grandes questões que encontro por ai é com relação a API do Membership do ASP.NET 2.0. Muitos desenvolvedores precisam incrementá-la, ou melhor, adicionar novas propriedades para que a mesma ganhe novas características, e atendam a necessidade da aplicação que está sendo construída. Essas propriedades são provenientes de tabelas diferentes das criadas pelo utilitário aspnet_regsql.exe ou ainda, em alguns casos, novas colunas que foram adicionadas dentro das tabelas aspnet_Users e aspnet_Membership.

Já no código, o que geralmente se costuma fazer, é criar uma classe que herde diretamente de MembershipUser:

public class Usuario : MembershipUser { ... }

... e, dentro desta, adicionar as propriedades que estão sendo desejadas. Depois disso, será necessário customizar o provider que estamos utilizando que, na maioria das vezes é o SqlMembershipProvider, e sobrescrever alguns métodos para adicionar um trabalho extra, onde devemos buscar os dados adicionais. Eis abaixo um exemplo:

public class CustomSqlMembershipProvider : SqlMembershipProvider
{
    public override MembershipUser GetUser(string userName, bool userIsOnline)
    {
        MembershipUser user = base.GetUser(userName, userIsOnline);
        CustomUser custom = new CustomUser(user);
        custom.Telefone = "+55 (19) 5555-5555";
        return custom;
    }
}

Mas as mudanças não param por ai. Como a interface da classe Membership opera somente com o tipo MembershipUser, voce será obrigado em todos os momentos que precisar chamar um determinado método (como o GetUser acima), efetuar conversões para o seu tipo específico. Algo mais ou menos como:

CustomUser custom = Membership.GetUser("israelaece", false) as CustomUser;
if(custom != null)
    Response.Write(custom.Telefone);

Ainda para piorar as coisas, voce perderá toda um dos principais benefícios do Provider Model que é a reutilização de código pois, se amanhã voce precisar rodar a aplicação sob uma base de dados Access ou Oracle, voce terá que sobrescrever cada um dos respectivos providers para dar o devido suporte.

Ao meu ver, eu acredito que a classe MembershipUser deve ser utilizada única e exclusivamente para gerenciar a autenticação/identificação do usuário dentro da aplicação. Se voce precisar adicionar características para os usuários, então a minha recomendação é utilizar o Profile (que não deixa ser uma extensão do usuário). Caso isso não seja possível, crie uma API baseando-se no modelo de Provider Model, se desejar suportar os benefícios que ele proporciona. Se mesmo assim, nenhuma dessas alternativas forem possíveis, então voce poderia criar uma classe que expõe as novas propriedades do usuário, populando-a da forma tradicional, operando de forma independente a classe MembershipUser.

Tags: ,

ASP.NET | Security

Membership: Geração/Reset de Password

by Israel Aece 26. January 2008 15:29

Se voce está utilizando as funcionalidades de autenticação do ASP.NET 2.0, então é provável que voce já se deparou com a necessidade de recuperar/redefinir (PasswordRecovery) a senha de uma determinado usuário. Há um detalhe importante a ser observado quando voce habilita esta funcionalidade.

O comportamento deste controle merece uma atenção especial para os dois cenários suportados:

  • enablePasswordRetrieval = true: quando esta opção está definida como true, obriga voce também a definir a propriedade passwordFormat para um valor diferente de Hashed. Se esta combinação de valores for atendida, a senha será recuperada da base de dados e enviada para o e-mail do respectivo usuário.
  • enablePasswordRetrieval = false: esta configuração é geralmente utilizada quando a propriedade passwordFormat é definida como Hashed, ou seja, não será possível recuperar a senha da base de dados, pois ela foi hasheada. Neste caso, quando o usuário utilizar o controle PasswordRecovery, então ele gerará uma nova senha e, é neste momento que começa toda a confusão.

O processo para a geração da nova senha invoca, indiretamente, o método GeneratePassword do provider selecionado que, na maioria dos casos, é o SqlMembershipProvider. Dentro deste método há uma chamada para o método estático GeneratePassword da classe Membership. A confusão ocorre aqui por causa de dois atributos que são configurados no arquivo Web.Config: minRequiredPasswordLength e minRequiredNonalphanumericCharacters. Esses valores servem apenas para validação de senhas informadas pelo usuário mas, para o processo de geração automática (reset), são desprezados.

O SqlMembershipProvider tem um comportamento padrão que é: se o valor informado no atributo minRequiredPasswordLength for menor que 14, ele assume 14; o valor do atributo minRequiredNonalphanumericCharacters é sempre informado mas, pela lógica implementada, não mudará em nada.

O que voce precisa fazer é sobrescrever o método GeneratePassword da classe SqlMembershipProvider e, colocar a regra necessária para mudar o comportamento padrão. Eis aqui uma possível implementação:

using System.Security.Cryptography;

public class CustomMembershipProvider : SqlMembershipProvider
{
    private static char[] chars = "a0bc1d2efgh3i4jkl5m6n7o8p9qrstuvuyzw".ToCharArray();

    public override string GeneratePassword()
    {
        if (this.MinRequiredNonAlphanumericCharacters == 0)
        {
            byte[] randomBytes = new byte[this.MinRequiredPasswordLength];
            char[] result = new char[this.MinRequiredPasswordLength];
            new RNGCryptoServiceProvider().GetBytes(randomBytes);

            for (int i = 0; i < this.MinRequiredPasswordLength; i++)
            {
                int index = ((int)randomBytes[i]) % chars.Length;
                result[i] = chars[index];
            }

            return new string(result);
        }
        else
        {
            return base.GeneratePassword();
        }
    }
}

//Configuração (Web.Config):
<membership defaultProvider="SqlMembershipProvider">
    <providers>
        <clear/>
        <add
            name="SqlMembershipProvider"
            type="CustomMembershipProvider"
            minRequiredNonalphanumericCharacters="0"
            minRequiredPasswordLength="5"/>
    </providers>
</membership>

Tags: ,

ASP.NET | Security

Powered by BlogEngine.NET 1.5.0.0
Theme by Mads Kristensen

Sobre

Meu nome é Israel Aece e sou especialista em tecnologias de desenvolvimento Microsoft, atuando como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. [ Mais ]

Twitter

Host