Cutting Edge

Primeiro contato com o ASP.NET Identity

Dino Esposito

Dino EspositoFruto da abordagem "Um ASP.NET" para desenvolvimento para a Web que surgiu com o Visual Studio 2013, o novo sistema ASP.NET Identity é o meio preferido de lidar com a autenticação do usuário nos aplicativos ASP.NET, seja com base no Web Forms ou no MVC. Nesta coluna, vou analisar os conceitos básicos da autenticação do ASP.NET e explorar o novo sistema ASP.NET Identity da perspectiva dos desenvolvedores do ASP.NET MVC 5.

Há bastante tempo, o ASP.NET oferece suporte a dois tipos básicos de autenticação: autenticação do Windows e autenticação de formulários. A autenticação do Windows é uma prática rara em sites públicos, pois se baseia nas contas do Windows e nos tokens de ACL (lista de controle de acesso). Desse modo, ela exige que os usuários tenham uma conta do Windows no domínio do aplicativo e também supõe que os clientes estejam se conectando de computadores com plataformas Windows. A outra opção é a autenticação de formulários, uma abordagem que foi amplamente adotada. A autenticação de formulários se baseia em um ideia simples. Para cada acesso a um recurso protegido, o aplicativo garante que a solicitação inclua um cookie de autenticação válido. Se for encontrado um cookie válido, a solicitação será atendida como de costume; caso contrário, o usuário será redirecionado para uma página de logon e terá que fornecer credenciais. Se essas credenciais forem reconhecidas como válidas, o aplicativo emitirá um cookie de autenticação com uma determinada política de expiração. É simples e funciona.

A implementação de qualquer módulo de autenticação de formulários não pode acontecer sem um módulo distinto que trate da coleta das credenciais do usuário e da verificação em um banco de dados de usuários conhecidos. Escrever esse subsistema de associação tem sido uma das principais responsabilidades das equipes de desenvolvimento, mas também uma das tarefas mais importunas. Escrever um sistema de associação não é difícil, por si só. Na maioria das vezes, essa tarefa exige a execução de uma consulta em algum tipo de sistema de armazenamento e a verificação de um nome de usuário e uma senha. Esse código é um estereótipo e pode aumentar bastante à medida que você adiciona novos recursos de autenticação, como alteração e recuperação de senhas, tratamento de um número em constante mudança de usuários online, etc. Além disso, ele precisará ser reescrito quase do zero se você alterar a estrutura do armazenamento ou adicionar mais informações ao objeto que descreve o usuário. De volta ao ano de 2005, com o lançamento do ASP.NET 2.0, a Microsoft resolveu esse problema introduzindo diretamente na estrutura uma arquitetura baseada no provedor e o provedor de associação. Em vez de recriar o esquema toda vez, você pode simplesmente gerar a associação do sistema interno e substituir apenas as funções que pretende alterar.

O provedor de associação nativo do ASP.NET é um componente autônomo que expõe uma interface contratada. O tempo de execução do ASP.NET, que orquestra o processo de autenticação, tem conhecimento da interface de associação e pode invocar qualquer componente que esteja configurado como provedor de associação do aplicativo. O ASP.NET apresenta um provedor de associação padrão que se baseia em um determinado esquema de banco de dados novo. No entanto, você pode escrever com facilidade seu próprio provedor de associação para atingir basicamente um banco de dados diferente, geralmente, um banco de dados de usuários existente.

Isso parece ser uma grande parte da arquitetura? No início, quase todos achavam que sim. No entanto, com o tempo, várias pessoas que tentavam repetidamente criar um provedor de associação personalizado começaram a reclamar sobre o detalhamento da interface. Na verdade, o provedor de associação se apresenta na forma de uma classe base hereditária, MembershipProvider, que inclui mais de 30 membros marcados como abstratos. Isso significa que para qualquer novo provedor de associação que você desejasse criar, havia, pelo menos, 30 membros a serem substituídos. O que é ainda pior, você realmente não precisava de muitos deles a maior parte do tempo. Havia necessidade de uma arquitetura de associação mais simples.

Introdução ao provedor de associação simples

Para poupar você da tarefa maçante de criar uma camada de associação personalizada completamente do zero, a Microsoft apresentou outra opção com o Visual Studio 2010 SP1: a API de associação simples. Originalmente disponível no WebMatrix e em Páginas da Web, a API de associação simples tornou-se um maneira bastante popular de gerenciar a autenticação, especialmente no ASP.NET MVC. Particularmente, o modelo de aplicativo de Internet no ASP.NET MVC 4 usa a API de associação simples para oferecer suporte ao gerenciamento e à autenticação de usuário.

Observando as subjacências da API, descobre-se que ela é apenas uma capa sobre a API de associação clássica do ASP.NET e seus repositórios de dados baseados no SQL Server. A associação simples permite que você trabalhe com qualquer repositório de dados que possua e exige apenas que você indique quais colunas na tabela têm a função de nome de usuário e ID de usuário.

A principal diferença da API de associação clássica é uma lista significativamente mais curta de parâmetros para quaisquer métodos. Além disso, você ganha muito mais liberdade no que diz respeito ao esquema de armazenamento de associação. Como um exemplo da API simplificada, considere o que é preciso para criar um novo usuário:

WebSecurity.CreateUserAndAccount(username, password,
  new { FirstName = fname, LastName = lname, Email = email });

Você executa a maioria das tarefas de associação usando a classe WebSecurity. No entanto, no ASP.NET MVC 4, a classe WebSecurity espera trabalhar com um provedor de associação estendido, e não com um provedor de associação clássico. Os recursos adicionais em um provedor de associação estendido são relacionados para lidar com contas OAuth. Consequentemente, no ASP.NET MVC 4, você tem dois roteiros paralelos para implementação da associação: a API de associação clássica usando a classe MembershipProvider e a API de associação simples usando a classe ExtendedMembershipProvider. As duas APIs são incompatíveis.

Antes da chegada do Visual Studio 2013 e do ASP.NET MVC 5, o ASP.NET já oferecia diversas maneiras de tratar a autenticação do usuário. Com a autenticação de formulários, você pode confiar na associação clássica, a API de associação simples, conforme definido nas Páginas da Web, e em uma variedade de sistemas de associação personalizados. Lembrando que a colocação comum entre especialistas do ASP.NET era a de que aplicativos complexos do mundo real exigem o próprio provedor de associação. Na maioria dos casos, o principal motivo de ter um sistema de associação personalizado era contornar as diferenças estruturais entre o formato exigido do banco de dados e o formato do banco de dados existente de credenciais de usuário, que poderia ter sido usado por anos.

Obviamente, essa não era uma situação que duraria para sempre. A comunidade de desenvolvedores pediu em alto e bom som um sistema unificado para associação, que fosse simples de usar, estreitamente focado e utilizável da mesma forma a partir de qualquer versão do ASP.NET. Essa ideia combinou bem com a abordagem de Um ASP.NET promovida pelo Visual Studio 2013.

Uma estrutura de identidade

A finalidade da autenticação é obter a identidade associada ao usuário atual. A identidade é recuperada e as credenciais fornecidas são comparadas aos registros armazenados em um banco de dados. Subsequentemente, um sistema de identidade, como o ASP.NET Identity, se baseia em dois blocos principais: o gerenciador de autenticação e o gerenciador de armazenamento. Na estrutura do ASP.NET Identity, o gerenciador de autenticação usa o formulário da classe UserManager<TUser>. Essa classe fornece basicamente uma fachada para que os usuários entrem e saiam. O gerenciador de armazenamento é uma instância da classe UserStore<TUser>. A Figura 1 mostra o esqueleto de uma classe de controlador de conta do ASP.NET MVC que se baseia no ASP.NET Identity.

Figura 1 Base de um controlador fundamentado no ASP.NET Identity

public class AccountController : Controller
{
  public UserManager<ApplicationUser> UserManager { get; private set; }
  public AccountController(UserManager<ApplicationUser> manager)
  {
    UserManager = manager;
  }
  public AccountController() :
    this(new UserManager<ApplicationUser>(
      new UserStore<ApplicationUser>(new ApplicationDbContext())))
  {
  }
  ...
}

O controlador mantém uma referência ao gerenciador de identidades de autenticação, o UserManager. A instância do UserManager é injetada no controlador. Você pode usar uma estrutura IoC (Inversão de Controle) ou a alternativa humana, o padrão DI (injeção de dependência), que usa dois controladores, um dos quais obtém um valor padrão (veja a Figura 1). 

O repositório de identidades, por sua vez, é injetado no gerenciador de identidades de autenticação, onde é usado para verificar credenciais. O repositório de identidades usa o formulário da classe UserStore<TUser>. Essa classe resulta da composição de vários tipos:

public class UserStore<TUser> :
  IUserLoginStore<TUser>,
  IUserClaimStore<TUser>,
  IUserRoleStore<TUser>,
  IUserPasswordStore<TUser>,
  IUserSecurityStampStore<TUser>,
  IUserStore<TUser>,
  IDisposable where TUser : IdentityUser
  {
  }

Todas as interfaces implementadas pela UserStore<TUser> são repositórios básicos para dados opcionais relacionados aos usuários, como senhas, funções, declarações e, é claro, dados de usuário. Entretanto, o repositório de identidades precisa saber sobre a fonte de dados real. Conforme mostrado na Figura 1, a fonte de dados é injetada na classe UserStore através do construtor.

O armazenamento de dados dos usuários é gerenciado pela abordagem Code First do Entity Framework. Isso significa que você não precisa exatamente criar um banco de dados físico para armazenar credenciais dos usuários. Em vez disso, você pode definir uma classe User e fazer com que a estrutura subjacente crie o banco de dados mais apropriado para armazenar tais registros.

A classe ApplicationDbContext encerra o contexto do Entity Framework para salvar dados dos usuários. Veja uma possível definição para a classe ApplicationDbContext:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
}

Basicamente, o contexto de banco de dados do ASP.NET Identity trata da persistência de um determinado tipo de usuário. O tipo de usuário deve implementar a interface IUser ou apenas herdar de IdentityUser. A Figura 2 apresenta o código-fonte da classe IdentityUser padrão.

Figura 2 Definição da classe User padrão no ASP.NET Identity

namespace Microsoft.AspNet.Identity.EntityFramework
{
  public class IdentityUser : IUser
  {
    public string Id { get; }
    public string UserName { get; set; }
    public string PasswordHash { get; set; }
    public string SecurityStamp { get; set; }
    public ICollection<IdentityUserRole> Roles { get; private set; }
    public ICollection<IdentityUserClaim> Claims { get; private set; }
    public ICollection<IdentityUserLogin> Logins { get; private set; }
  }
}

Veja um exemplo de uma classe de usuário personalizada realista que talvez você queira usar em seus aplicativos:

public class ApplicationUser : IdentityUser{
  public DateTime Birthdate { get; set; }
}

O uso de Code First do Entity Framework é uma grande jogada aqui, pois torna a estrutura do banco de dados um ponto secundário. Você ainda precisa de uma, mas para criá-la, você pode usar código baseado em classes. Além disso, é possível usar as ferramentas de migração do Code First do Entity Framework para modificar um banco de dados anteriormente criado, à medida que você faz alterações na classe por trás dele. (Para obter mais informações sobre isso, consulte o artigo "Migrações do Code First" na Central de Desenvolvedores de Dados do MSDN em bit.ly/Th92qf.)

Autenticando usuários

O ASP.NET Identity é baseado no mais recente middleware de autenticação OWIN (Interface Open Web para .NET). Isso significa que as etapas típicas de autenticação (por exemplo, criação e verificação de cookies) podem ser executadas por meio de interfaces OWIN abstratas, e não diretamente pelas interfaces ASP.NET/IIS. O suporte para OWIN exige que o controlador de conta tenha outra propriedade útil, como esta:

private IAuthenticationManager AuthenticationManager
{
  get {
  return HttpContext.GetOwinContext().Authentication;
  }
}

A interface IAuthenticationManager é definida no namespace Microsoft.Owin.Security. Essa propriedade é importante porque precisa ser injetada em qualquer operação que envolva etapas relacionadas à autenticação. Veja um típico método de logon:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
  var identity = await UserManager.CreateIdentityAsync(user,
    DefaultAuthenticationTypes.ApplicationCookie);
  AuthenticationManager.SignIn(new AuthenticationProperties() {
    IsPersistent = isPersistent }, identity);
}

O método SignInAsync verifica o nome de usuário e a senha especificados no repositório associado ao gerenciador de autenticação. Para registrar um usuário e adicioná-lo ao banco de dados de associação, você pode usar um código como este:

var user = new ApplicationUser() { UserName = model.UserName };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
  await SignInAsync(user, isPersistent: false);
  return RedirectToAction("Index", "Home");
}

De modo geral, o ASP.NET Identity fornece uma API unificada para tarefas relacionadas à autenticação. Por exemplo, ele unifica o código exigido para autenticação em um banco de dados proprietário ou em um ponto de extremidade baseado em OAuth de rede social. A Figura 3 mostra um fragmento do código que você precisa para autenticar usuários em um mecanismo de logon externo. O código na Figura 3 é chamado assim que a autenticação OAuth (por exemplo, no Facebook) é concluída com êxito.

Figura 3 Finalizando o processo de autenticação por meio de um ponto de extremidade externo

public async Task<ActionResult> ExternalLoginCallback(
  string loginProvider, string returnUrl)
{
  ClaimsIdentity id = await UserManager
  .Authentication
  .GetExternalIdentityAsync(AuthenticationManager);
  var result = await UserManager
    .Authentication
    .SignInExternalIdentityAsync(
      AuthenticationManager, id);
  if (result.Success)
    return RedirectToLocal(returnUrl);
  else if (User.Identity.IsAuthenticated)
  {
    result = await UserManager
      .Authentication
      .LinkExternalIdentityAsync(
        id, User.Identity.GetUserId());
    if (result.Success)
      return RedirectToLocal(returnUrl);
    else
      return View("ExternalLoginFailure");
  }
}

Conclusão

No meu entender, o ASP.NET Identity é uma solução atrasada que deveria ter surgido anos atrás. O principal problema relacionado ao ASP.NET Identity no momento é que a equipe de desenvolvimento está tentando encontrar uma interface de programação que seja genérica e possa ser testada o suficiente para durar um longo tempo ou, pelo menos, até algo mais novo e melhor surgir no setor.

Para o futuro próximo, o ASP.NET Identity promete ser tão bom quanto à associação conservadora de uma década atrás. Pessoalmente, gosto da expressividade da API e da tentativa de combinar diferentes formas de autenticação (com base no OAuth e internas, por exemplo). Outra grande adição é a integração com o OWIN, que a torna, de certo modo, independente de um tempo de execução específico, como o IIS/ASP.NET.

O ASP.NET Identity foi associado ao Visual Studio 2013, mas também se espera que tenha uma vida autônoma quando se trata de compilações e versões futuras. Minha abordagem da nova API de identidade foi apenas superficial. Fique atento às novas compilações e versões!

Dino Esposito é o autor de “Architecting Mobile Solutions for the Enterprise” (Microsoft Press, 2012) e de “Programming ASP.NET MVC 5”, que será lançado em breve pela Microsoft Press. Divulgador técnico das plataformas .NET e Android no JetBrains e palestrante frequente em eventos do setor no mundo todo, Esposito compartilha sua visão de software em software2cents.wordpress.com e no Twitter, em twitter.com/despos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Pranav Rastogi (Microsoft)