Cutting Edge

Autenticação externa com o ASP.NET Identity

Dino Esposito

Dino EspositoO Visual Studio 2013 inclui novos modelos de projeto do ASP.NET MVC 5 e do Web Forms para desenvolver aplicativos. Os modelos de exemplo podem conter opcionalmente uma camada de autenticação padrão com base no ASP.NET Identity. O código gerado para você contém vários recursos, mas talvez ele não seja tão fácil de ler. Neste artigo, mostrarei como executar uma autenticação por meio de um servidor baseado no OAuth ou OpenID externos, como uma rede social e os passos que você pode desejar dar assim que a autenticação estiver concluída e você tiver acesso a todas as declarações baixadas.

Começarei com um aplicativo ASP.NET MVC 5 vazio e adicionarei uma camada de autenticação mínima que um aplicativo pode precisar. Você verá que a nova estrutura do ASP.NET Identity traz um novo nível de complexidade e poder para toda a pilha de autenticação, mas ao mesmo tempo, torna mais rápido e fácil organizar a autenticação do usuário externo sem a necessidade de você aprender novos pacotes e APIs.

Criando o Controlador de Conta

Desde o primeiro lançamento do ASP.NET MVC, o aplicativo de exemplo que representa a face da estrutura para os novos usuários ofereceu uma classe de controlador particularmente inflada para gerenciar todos os aspectos da autenticação. Na minha coluna de março de 2014, “Primeiro contato com o ASP.NET Identity” (msdn.microsoft.com/magazine/dn605872), forneci uma visão geral da estrutura do ASP.NET Identity começando pelo código que você obtém por padrão no Visual Studio. Desta vez, ao invés, vou colocar várias partes juntas começando por um projeto do ASP.NET MVC 5 vazio. Ao fazer isso, vou seguir cuidadosamente o Princípio da responsabilidade única (bit.ly/1gGjFtx).

Iniciarei com um projeto vazio e depois adicionarei algum scaffolding do ASP.NET MVC 5. Depois, adicionarei duas novas classes de controlador: Login­Controller e AccountController. O anterior está apenas preocupado com as operações de entrada e saída, quer sejam realizadas por meio de um site específico, sistema de associação local ou um provedor OAuth, como o Twitter. Como o próprio nome sugere, a classe AccountController inclui qualquer outra funcionalidade principalmente relacionada ao gerenciamento das contas de usuário. Para fins de simplicidade, eu apenas incluirei a capacidade de registrar um novo usuário desta vez. A Figura 1 mostra o esqueleto das duas classes do controlador, como aparecem no Visual Studio 2013.

Skeleton of LoginController and AccountController Classes in Visual Studio 2013
Figura 1 Esqueleto das Classes LoginController e AccountController no Visual Studio 2013

A interface do usuário está articulada em três exibições parciais: usuário registrado, formulário de logon local e formulário de logon social, conforme exibido na Figura 2.

The Three Partial Views Used in the Authentication Stack
Figura 2 As três exibições parciais usadas na pilha de autenticação

A Classe IdentityController

No esqueleto exibido na Figura 1, ambas as classes LoginController e AccountController herdam da classe do Controlador base ASP.NET MVC 5. Se você vai usar a estrutura do ASP.NET Identity, no entanto, isto talvez não seja a escolha ideal, já que pode levar a algum código duplicado. Sugiro que você crie um projeto temporário no Visual Studio 2013 e olhe o código padrão armazenado na classe poderosa e abrangente AccountController. O ASP.NET Identity exige que você insira a classe UserManager<TUser> e o mecanismo de armazenamento no controlador. Além disso, talvez você queira ter uns dois métodos auxiliares para simplificar o processo de conectar e redirecionar os usuários. Você também precisa de algumas propriedades para referenciar o objeto raiz UserManager e talvez ainda outra propriedade auxiliar para estabelecer um ponto de contato com a interface subjacente Open Web para o middleware .NET (OWIN).

Provavelmente, é uma boa ideia introduzir uma classe intermediária onde todos estes códigos clichês podem ser confortavelmente acomodados uma vez e somente uma vez. Chamarei esta classe de IdentityController e definirei ela conforme exibido na Figura 3.

Figura 3 A Nova classe base para a autenticação do ASP.NET

using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using AuthSocial.ViewModels.Account.Identity; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; namespace AuthSocial.Controllers { public class IdentityController<TUser, TDbContext> : Controller where TUser : IdentityUser where TDbContext : IdentityDbContext, new() { public IdentityController() : this(new UserManager<TUser>( new UserStore<TUser>(new TDbContext()))) { } public IdentityController(UserManager<TUser> userManager) { UserManager = userManager; } protected UserManager<TUser> UserManager { get; set; } protected IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } protected async Task SignInAsync( TUser user, bool isPersistent) { AuthenticationManager.SignOut( defaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, identity); } protected ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim(ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); } } }

O uso de restrições relacionadas e gerais, vincula a classe IdentityController para usar qualquer definição de um usuário com base na classe IdentityUser e qualquer definição de um contexto de armazenamento com base no IdentityDbContext.

O IdentityUser, em particular, é apenas uma classe base para descrever um usuário e oferecer um conjunto mínimo de propriedades. Você é bem-vindo para criar classes novas e mais específicas por meio da herança simples. Quando você faz isso, você apenas especifica a sua classe personalizada na declaração de classes derivadas do IdentityController. Aqui está a declaração do LoginController e AccountController com base na classe pública IdentityController<TUser, TDbContext>:

LoginController : IdentityController<IdentityUser, IdentityDbContext> { ... } public class AccountController : IdentityController<IdentityUser, IdentityDbContext> { ... }

Desta maneira, você define o caminho para o particionamento da maioria da complexidade da autenticação e do código de gerenciamento do usuário pelas três classes. Além do mais, você agora tem uma classe para fornecer serviços comuns e uma classe que apenas foca nas tarefas de logon. Finalmente, você tem uma classe separada para lidar com o gerenciamento de contas de usuário. Vou começar com a classe de conta, onde, para fins de simplicidade, eu apenas exponho um ponto de extremidade para registrar novos usuários.

A Classe AccountController

Geralmente, você precisa de um método de controlador somente se há uma parte da interface do usuário exibida que o usuário final interage. Se há, vamos dizer, um botão de envio que o usuário pode clicar, então você precisa de um método de controlador sensível ao POST. Para definir este método, inicie do modelo de exibição para ele. A classe do modelo de exibição é um Objeto de Transferência de Dados (DTO) simples que reúne qualquer tipo de dados (strings, datas, números, booleanos e coleções) que entram e saem da interface do usuário exibida. Para um método que adiciona um novo usuário no sistema de associação local, você pode ter a seguinte classe de modelo de exibição (o código-fonte é o mesmo que você obtém do modelo padrão do Visual Studio 2013 ASP.NET MVC 5):

public class RegisterViewModel { public string UserName { get; set; } public string Password { get; set; } public string ConfirmPassword { get; set; } }

Talvez você também queira decorar esta classe com anotações de dados para fins de validação e exibição. Em um nível mais alto de design, tudo o que conta é que a classe apresenta uma propriedade para cada controle de entrada na interface do usuário real. A Figura 4 apresenta a implementação do método de controlador que manipula a solicitação POST para registrar um novo usuário.

Figura 4 Como registrar um novo usuário

public async Task<ActionResult> Register( RegisterViewModel model) { if (ModelState.IsValid) { var user = new IdentityUser { UserName = model.UserName }; var result = await UserManager.CreateAsync( user, model.Password); if (result.Succeeded) { await SignInAsync(user, false); return RedirectToAction("Index", "Home"); } Helpers.AddErrors(ModelState, result); } return View(model); }

O método CreateAsync na classe UserManager apenas usa o mecanismo de armazenamento subjacente para criar uma nova entrada para o usuário especificado. Esta classe AccountController é também o local ideal para definir outros métodos para editar ou apenas excluir contas de usuário e alterar ou redefinir senhas.

A classe LoginController

O controlador de logon apresentará métodos para conectar e desconectar usuários de qualquer forma que seja aceitável para o aplicativo. Semelhantemente ao que eu tinha discutido sobre o controlador de conta, o método SignIn gerenciará um DTO com propriedades para carregar credenciais, um sinalizador para a persistência de autenticação e talvez uma URL de retorno. No código padrão, a URL de retorno é gerenciada por meio de ViewBag; você também pode colocá-la na classe de modelo de exibição:

public class LoginViewModel { public string UserName { get; set; } public string Password { get; set; } public bool RememberMe { get; set; } public string ReturnUrl { get; set; } }

O código para interagir com o sistema de associação local, totalmente gerenciado no servidor do aplicativo, é colado a partir do projeto padrão. Veja a seguir um extrato da implementação POST do novo método SignIn:

var user = await UserManager.FindAsync( model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); }

O código que no final conecta os usuários é um método—denominado SignInAsync—emprestado de uma implementação padrão e reescrito como um método protegido na classe base IdentityController:

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

Ao usar a estrutura do ASP.NET, você não usa nenhuma classe que está explicitamente relacionada ao mecanismo do ASP.NET. No contexto de um host do ASP.NET MVC, você ainda gerencia a autenticação de usuário por meio de formulários de logon e cookies de autenticação do ASP.NET—exceto que você não usa a classe FormsAuthentication ASP.NET diretamente. Há duas camadas de código com as quais desenvolvedores clássicos do ASP.NET precisam enfrentar. Uma é a API do ASP.NET Identity simples representada pelas classes UserManager e IdentityUser. A outra é o cookie de middleware OWIN, que apenas protege a API de autenticação de fachada dos detalhes essenciais de como informações do usuário conectado são armazenadas e mantidas. A autenticação por formulários ainda é usada, e o velho conhecido, o cookie .ASPXAUTH ainda é criado. No entanto, tudo agora acontece atrás da cortina de vários novos métodos. Para um entendimento mais aprofundado sobre isto, consulte a postagem do blog de Brock Allen, “A Primer on OWIN Cookie Authentication Middleware for the ASP.NET Developer,” em bit.ly/1fKG0G9.

Observe o seguinte código que você encontra invocado diretamente do Startup­Auth.cs, o ponto de entrada para o middleware OWIN:

app.UseCookieAuthentication( new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Login/SignIn") }); app.UseExternalSignInCookie( DefaultAuthenticationTypes.ExternalCookie); app.UseTwitterAuthentication( consumerKey: "45c6...iQ", consumerSecret: "pRcoXT...kdnU");

Este código é um tipo de interface fluente que você usa ao invés das entradas de autenticação antigas do web.config. Além disso, ela também vincula na autenticação baseada no Twitter e configura o middleware OWIN para usar o ExternalSignInCookie para armazenar temporariamente informações sobre um usuário se conectar por meio de um provedor de logon de terceiros.

O objeto AuthenticationManager representa o ponto de entrada no middleware OWIN e na fachada por meio do qual você invoca a API de autenticação subjacente ASP.NET baseada na FormsAuthentication.

O resultado é que você acaba usando uma API unificada para autenticar usuários e verificar credenciais quer o sistema de associação seja local para seu servidor Web ou delegado externamente para um provedor de logon de rede social. Este é o código que você usar se for aceitável que os usuários do seu site autentiquem por meio de suas contas do Twitter:

[AllowAnonymous] public ActionResult Twitter(String returnUrl) { var provider = "Twitter"; return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Login", new { ReturnUrl = returnUrl })); }

Antes de mais nada, você precisa ter um aplicativo do Twitter configurado e associado a uma conta de desenvolvedor do Twitter. Para obter mais informações, confira o dev.twitter.com. Um aplicativo do Twitter é identificado exclusivamente por um par de sequências de caracteres alfanuméricos conhecidos como chave e segredo do consumidor.

O código padrão que você recebe do assistente de projeto ASP.NET MVC 5 também sugeri que você use uma classe completamente nova—ChallengeResult—para manipular logons externos, mostrados na Figura 5.

Figura 5 A classe ChallengeResult

public class ChallengeResult : HttpUnauthorizedResult { public ChallengeResult(string provider, string redirectUri) { LoginProvider = provider; RedirectUri = redirectUri; } public string LoginProvider { get; set; } public string RedirectUri { get; set; } public override void ExecuteResult(ControllerContext context) { var properties = new AuthenticationProperties { RedirectUri = RedirectUri }; context.HttpContext .GetOwinContext() .Authentication .Challenge(properties, LoginProvider); } }

A classe delega ao middleware OWIN—no método chamado Challenge—a tarefa para conectar ao provedor de logon especificado (registrado na inicialização) para realizar a autenticação. A URL de retorno é o método ExternalLoginCallback no controlador de logon:

[AllowAnonymous] public async Task<ActionResult> ExternalLoginCallback( string returnUrl) { var info = await AuthenticationManager.GetExternalLoginInfoAsync(); if (info == null) return RedirectToAction("SignIn"); var identity = GetBasicUserIdentity(info.DefaultUserName); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = true }, identity); return RedirectToLocal(returnUrl); }

O método GetExternalLoginInfoAsync recupera as informações sobre o usuário identificado que o provedor de logon torna disponível. Quando o método retorna, o aplicativo tem informações suficientes para realmente conectar o usuário, mas ainda não aconteceu nada. É importante estar ciente das opções que você tem aqui. Primeiro, você pode prosseguir e criar um cookie de autenticação canônico do ASP.NET, como no meu exemplo. Definido na classe base IdentityController, o método auxiliar GetBasicUserIdentity cria um objeto ClaimsIdentity em volta do nome da conta do Twitter fornecido:

protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim( ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); }

Quando o usuário do Twitter autoriza o aplicativo do Twitter a compartilhar informações, a página da Web recebe o nome do usuário e possivelmente mais dados, conforme exibido na Figura 6 (isto depende da API social real que está sendo usada).

The Classic Twitter-Based Authentication
Figura 6 A autenticação clássica baseada no Twitter

Observe no código anterior que no método ExternalLoginCallback nenhum usuário é adicionado ao sistema de associação local. Esse é o cenário mais simples. Em outras situações, você talvez queira usar as informações do Twitter para registrar programaticamente um link entre um logon local e um nome de usuário autenticado externamente (esta é a solução apresentada no código de autenticação padrão do assistente). Finalmente, você pode decidir redirecionar os usuários para uma página diferente para deixá-los inserir um endereço de email ou apenas registrarem-se no seu site.

Como misturar a autenticação clássica e a social

Algum nível de autenticação é necessário em quase todos os sites. O ASP.NET Identity torna fácil codificar e misturar a autenticação social e clássica atrás de uma fachada unificada. No entanto, é importante que você tenha uma estratégia clara sobre a maneira que deseja gerenciar as contas para os usuários e quais informações deseja coletar e armazenar sobre eles. Misturar a autenticação clássica e a social significa que você pode ter uma conta de usuário individual e associá-la a vários provedores de logon, como o Facebook, Twitter e todos os outros provedores de logon suportados pelo ASP.NET Identity. Desta maneira, os usuários têm várias maneiras de se conectarem, mas eles permanecem exclusivos aos olhos de seu aplicativo. E mais provedores de logon podem ser adicionados ou removidos conforme for conveniente.

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 Framework 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 da Microsoft pela revisão deste artigo: Pranav Rastogi