Esses dias eu tive que resolver um probleminha chato durante uma consultoria, e um que já tinha visto outras pessoas solicitando ajuda. Como após alguma pesquisa consegui montar um modelo legal, coloco aqui para ajudar quem vier a precisar de uma solução que funciona.
O problema é o seguinte: preciso, dentro de uma mesma aplicação, autenticar com forms authentication e windows authentication. No meu cenário, se o usuário vem da intranet, ele está na rede interna, e autentica com windows authentication usando sua conta de domínio, nem vendo nenhum formulário de login. Se está no mesmo site, só que fora da empresa, portanto na extranet, ele deve se autenticar via forms authentication, e não deve ver nenhuma requisição do navegador por credenciais windows.
Como fazer isso?
A solução inicial veio deste post, mas senti que ela estava incompleta, um pouco desatualizada, e não atendia ASP.NET MVC. O modelo que fiz está mais fácil de entender. Além disso, não é uma informação que se encontra fácil, e achei que valia a pena ter ela aqui, também em português.
A ideia é até que simples. Em primeiro lugar você precisa aceitar que não é possível ter os dois, windows e forms authentication, na mesma aplicação. Isso acontece porque o modelo que eles usam é diferente. Para windows authentication, ao acessar um recurso seguro, que demanda autenticação, você recebe um código http 401 (unauthorized). Isso acontece porque os navegadores já sabem responder ao 401, ao receber o código eles refazem o request só que desta vez enviam um token com as credenciais do usuário, que nem percebe que isso ocorreu. Já com o forms authentication, você recebe um redirect simples, com um 302 que indica onde está o formulário de autenticação. Faz sentido, e funciona bem, mas não se você quiser usar os dois juntos.
Por esse motivo, você tem que optar. Ao optar por forms, sempre que tentar acessar algo seguro você será direcionado para uma página de login. Com Windows, a infra do servidor cuida em requisitar os dados por você. Isso quer dizer que se trabalhar com forms auth, você tem a chance de interagir com o processo, enquanto que com windows auth você não tem. A escolha é óbvia, vamos usar forms auth.
Mas se escolhemos forms auth, como vamos fazer pra mandar o 401? Qualquer rota segura acessada vai indicar um 302 para a página de login. A solução, como em muitos casos, é adicionar mais uma camada de indireção. Vamos trabalhar a ação de login para avaliar: de onde vem o request? É da intranet? devolve um 401. É da extranet? Devolve um 302 e indica o usuário a se autenticar via formulário.
Só tem um problema: se uma ação sua retornar um 401, automaticamente o usuário recebe um 302 no lugar, porque o forms auth não permite chegar um 401 no usuário. Nem adianta tentar, mas se você quiser, crie uma aplicação com forms auth e tente criar uma ação assim:
public ActionResult TentandoPassar401() { return new HttpStatusCodeResult(HttpStatusCode.Unauthorized); }
Você vai notar que vai cair no formulário de login.
E agora? Como desabilitar o forms auth só em uma lugar específico para conseguir mandar o 401 e autenticar o usuário com a conta do windows? A solução é alterar o módulo de autenticação do forms authentication para que ele permita ligar ou desligar a autenticação via forms. Infelizmente, o módulo não permite esse tipo de customização. Resta então fazer um wrapper. O código está no github, então depois vocês revisam lá como fica o módulo. Olhando o web.config, dá pra entender o que houve. Primeiro desabilitamos o módulo original e habilitamos o novo:
<modules runAllManagedModulesForAllRequests="true"> <remove name="FormsAuthentication" /> <add name="FormsAuthentication" type="WindowsFormsAuth.WebApp.Infra.Auth.MixedFormsWindowsAuthModule, WindowsFormsAuth.WebApp, Version=1.0.0.0, Culture=neutral" /> </modules>
E em seguida desabilitamos forms authentication na rota de login do windows:
<location path="Conta/WindowsLogin"> <mixedFormsWindowsAuth formsAuthenticationEnabled="false" /> <system.webServer> <security> <authentication> <windowsAuthentication enabled="true" /> <anonymousAuthentication enabled="false" /> </authentication> </security> </system.webServer> </location>
A rota de login é a seguinte:
<authentication mode="Forms"> <forms loginUrl="~/Conta/Login" cookieless="UseCookies" /> </authentication>
Ou seja, AccountController, método Login. É nesse método que verificamos de onde vem o request:
public ActionResult Login(string returnUrl) { if (HttpContext.User.Identity.IsAuthenticated) return !string.IsNullOrWhiteSpace(returnUrl) ? (ActionResult) Redirect(returnUrl) : RedirectToAction("Index", "Home"); if (RequestVemDaExtranet()) return RedirectToRoute("Default", new { action = "WebLogin", returnUrl = returnUrl }); return RedirectToRoute("Default", new { action = "WindowsLogin", returnUrl = returnUrl }); }
O método WindowsLogin, como está configurado para funcionar com Windows Authentication, já recebe o usuário. Ele só captura o login de uma variável de ambiente e seta este login como o correto na infraestrutura do Forms Authentication. Dessa forma, conseguimos, de fato, unificar os dois modelos:
public ActionResult WindowsLogin(string returnUrl) { var usuario = Request.ServerVariables["LOGON_USER"]; if (string.IsNullOrWhiteSpace(usuario)) { return new HttpStatusCodeResult(HttpStatusCode.Unauthorized); } FormsAuthentication.SetAuthCookie(usuario, true); if (!string.IsNullOrWhiteSpace(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); }
Ao verificar se o usuário é nulo estamos garantindo que não teremos um usuário não autenticado passando, é apenas uma verificação de segurança, porque ao chegar no método a variável LOGON_USER do servidor sempre está preenchida.
O método WebLogin simplesmente exibe um formulário de Login, que ao postado envia o usuário e senha à mesma ação WebLogin. Lá verificamos usuário e senha:
[HttpPost] public ActionResult WebLogin(string usuario, string senha, bool lembreDeMim, string returnUrl) { if (ModelState.IsValid) { if (WindowsAuthenticate(usuario, senha)) { FormsAuthentication.SetAuthCookie(usuario, lembreDeMim); return !string.IsNullOrWhiteSpace(returnUrl) ? (ActionResult) Redirect(returnUrl) : RedirectToAction("Index", "Home"); } } return View(); } private static bool WindowsAuthenticate(string usuario, string senha) { using (var context = new PrincipalContext(ContextType.Machine, "nomedasuamaquina")) { return context.ValidateCredentials(usuario, senha); } }
O resultado é o mesmo, independente se o usuário se autentica via forms auth, ou via windows auth.
A solução está no github. Espero que isso ajude quem está na busca de algo parecido.
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.