É comum que certas áreas de nossos aplicativos sejam restritas apenas a usuários autenticados. E oferecer esse recurso em .NET é bem simples, mas eu já vi cada coisa por aí que resolvi mostrar pra vocês a forma correta de se fazer.

Primeiro, vamos analizar uma arquitetura comum de um software. O modelo abaixo mostra os principais componentes encontrados em uma aplicação .NET. Vamos levantar alguns questionamentos, para entedermos a real necessidade de uma segurança robusta.

image

  • A sua camada de regras de negócio é reutilizada através de várias camadas de apresentação?
  • A sua camada de regras de negócio contém funcionalidades que são restritas apenas a determinados grupos de usuários?
  • A sua camada de regras de negócio é usada em integrações com produtos legados?
  • A sua camada de apresentação deverá oferecer funcionalidades específicas para determinados grupos de usuários?
  • Você deseja uma segurança robusta e flexível?

Se você disse sim para a maioria das perguntas acima, você precisa aplicar segurança nas várias camadas do seu software. Agora vamos conhecer o que o .NET Framework tem a nos oferecer.

  • Segurança aplicada no contexto de execução. Como assim? Podemos marcar partes do nosso código que só podem ser executadas se o usuário do Thread possuir determinadas roles ou se encaixar em determinado grupo.
  • Possibilidade de acessar o ID do usuário em qualquer camada que desejarmos.
  • Permite atribuirmos Roles ao usuário. Podemos assim identificar perfis de acesso.

Vamos colocar a mão na massa e aplicar esses conceitos em um aplicativo web. Na camada de apresentação, adicione no web.config o seguinte código para negar acesar a usuários não autenticados e definir qual é a página de login:

<configuration>
  <system.web>
    <authorization>
      <deny users="?" />
    </authorization>
    <authentication mode="Forms">
      <forms name=".ASPXAUTH" loginurl="signin.aspx" defaulturl="default.aspx" protection="All" requiressl="false" timeout="30" slidingexpiration="true" path="/" enablecrossappredirects="false" cookieless="UseDeviceProfile">
      </forms>
    </authentication>
  </system.web>
</configuration>

A nossa página deverá conter os campos de Nome de Usuário e Senha, RequiredFieldValidators e um CustomValidator para validar o usuário e senha:

Nome: <asp:RequiredFieldValidator ID="rfvUserName" runat="server" ErrorMessage='Nome de usuário em branco'  Text='Nome em branco' ControlToValidate="txtUserName" />
<asp:CustomValidator ID="cvName" runat="server" Text='*' ControlToValidate="txtUserName" ErrorMessage='Nome de usuário ou senha inválida.' OnServerValidate="cvName_ServerValidate" />
<asp:TextBox ID="txtUserName" runat="server" />

Senha: <asp:RequiredFieldValidator ID="rfvPassword" runat="server" ErrorMessage='Senha em branco' Text='*' ControlToValidate="txtPassword" />
<asp:TextBox ID="txtPassword" runat="server" TextMode="Password" />

Deverá também ter um botão para executar o submit:

<asp:Button ID="btnSave" runat="server" Text='Login OnClick="btnSave_Click" />

Começando do Page_Load, adicionamos o seguinte código:

protected void Page_Load(object sender, EventArgs e)
{
    if (Request.IsAuthenticated)
        Response.Redirect(FormsAuthentication.DefaultUrl);
}

Temos também que criar uma função que muda o status do usuário para autenticado.

///

/// Cria o ticket de autenticação
/// 

/// Nome do usuário
/// Se true o cookie não irá expirar
/// Dados do perfil do usuário
/// Data de expiração do cookie
public static void CreateTicket(string username, bool isPersistent, string userData, DateTime expiration)
{
    FormsAuthentication.Initialize();

    //
    // Cria o ticket de autenticação
    //
    var ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, expiration, isPersistent, userData, FormsAuthentication.FormsCookiePath);

    //
    // Criptografa o ticket
    //
    string hash = FormsAuthentication.Encrypt(ticket);

    //
    // Guarda o cookie no navegador de acordo com as opções do usuário
    //
    var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, hash);

    if (ticket.IsPersistent)
        cookie.Expires = ticket.Expiration;

    HttpContext.Current.Response.Cookies.Add(cookie);
}

No evento ServerValidade do nosso CustomValidator, precisamos escrever um código pra validar se os dados estão corretos. Veja um pseudocódigo:

protected void cvName_ServerValidate(object source, ServerValidateEventArgs args)
{
    args.IsValid = txtUserName.Text == "admin" && txtPassword.Text == "admin";
}

No evento click do botão entrar, precisamos chamar nossos métodos. Veja, que optei por não consumir banco de dados para simplificar nosso artigo.

protected void btnSave_Click(object sender, EventArgs e)
{
    Validate();

    if (IsValid)
    {
        string userData = "admin|root|editor";

        CreateTicket(txtUserName.Text, chkCreatePersistCookie.Checked, userData, DateTime.Now.AddMinutes(30));

        string returnUrl = Request.QueryString["ReturnUrl"] ?? FormsAuthentication.DefaultUrl;

        Response.Redirect(returnUrl);
    }
}

Com os códigos acima, nós conseguimos autenticar o usuário. Agora precisamos escrever alguns códigos relacionados a autorização. Vamos a eles. No global.asax da nossa aplicação web precisamos definir que o Thread em execução pertence ao nosso usuário autenticado, fazemos isso da seguinte forma:

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
    if (Request.IsAuthenticated)
    {
        //
        // Pegamos as roles que estão guardadas no cookie
        //
        HttpCookie cookie = Request.Cookies[FormsAuthentication.FormsCookieName];
        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
        string[] roles = ticket.UserData.Split('|');

        //
        // Criamos a identidade do usuário
        //
        var principal = new GenericPrincipal(HttpContext.Current.User.Identity, roles);

        //
        // Define o contexto atual como sendo executado pelo nosso user
        //
        Thread.CurrentPrincipal = HttpContext.Current.User = principal;
    }
}

O código acima, pega os dados que estão armazenados no cookie e cria um objeto GenericPrincipal (armazena o id do usuário + roles) e associa ao Thread em execução. Com isso, podemos criar validações diretamente nos métodos de nossas classes. Vamos ver como fazer isso. Vamos supor que você tenha um método que somente usuários autenticados podem executar. Olha como fica nossa validação:

[PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
public bool ChangePasswordQuestionAndAnswer(string password, string newPasswordQuestion, string newPasswordAnswer)
{
    ...
    return false;
}

O .NET Framework irá se encarregar de verificar se o usuário está autenticado, e caso não esteja ele irá lançar uma SecurityException. Podemos também ter um método que queremos que apenas usuários administradores possam executar. Vamos ver como ficaria:

[PrincipalPermission(SecurityAction.Demand, Role = "admin")]
public void Unlock()
{
    ...
}

Viram como essa abordagem é mais consistente? Dúvidas nos comentários.