Estamos estos días planteando la migración de nuestra intranet a ASP Core MVC (quien dice «migración» dice «nueva intranet»). El primer reto a resolver es desentrañar los misterios de la autenticación. Es decir, cómo lo vamos a gestionar. Tenemos que validar a nuestros usuarios contra Active Directory, pero no nos vale la autenticación de Windows (o, por lo menos, no nos vale con la información que he encontrado; que luego igual resulta que sí nos servía, pero la documentación oficial es tremendamente parca): no sólo tenemos equipos con Windows en la red, también MacOS, y teléfonos, y tablets. Y equipos en fábrica con usuarios genéricos de Windows y luego la persona que lo usa en ese momento utiliza sus credenciales para entrar en las aplicaciones. Y equipos fuera de nuestro dominio.
Por todo esto, me centré en la autenticación general por cookies, por ser más flexible. La pregunta es: ¿cómo hacerlo?
Cómo no, un apunte en Stack Overflow me puso sobre la pista: utilizar las clases que nos permiten manipular Active Directory y que están en el paquete NuGet System.DirectoryServices.AccountManagement y cuya documentación podemos encontrar aquí.
- PrincipalContext nos da acceso al dominio y nos ofrece el método ValidateCredentials.
- Los usuarios están representados por la clase UserPrincipal, que tiene un método compartido FindByIdentity que nos devuelve el usuario para la identidad pasada.
- De los datos del usuario puedo montarme una lista de Claims y ya estoy en el caso del ejemplo de autenticación por cookie.
Con esto, ya puedo montar un laboratorio de pruebas.
Comienzo con una aplicación MVC sin autenticación y me creo un interfaz sencillo para definir el servicio de autenticación:
public interface IAuthenticationManager { Task<bool> Authenticate(string username, string password); }
Añado para la gestión del login un controlador, una vista con el formulario de login y un modelo sencillo con nombre de usuario y contraseña. El controlador quedaría algo así:
public class LoginController : Controller { private readonly IAuthenticationManager authenticationManager; public LoginController(IAuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } public IActionResult Index() { return RedirectToAction("Index", "Home"); } [AllowAnonymous] public ActionResult Login() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(UserCred model) { try { var user = await this.authenticationManager.Authenticate(model.Username, model.Password); return RedirectToAction(nameof(Index)); } catch { return View(); } } public async Task<IActionResult> Logoff() { if(HttpContext.User.Identity.IsAuthenticated) { await HttpContext.SignOutAsync(); } return RedirectToAction("Index", "Home"); } }
Lo siguiente es la clase para el servicio. Voy a necesitar el PrincipalContext y, también, acceso al HttpContext para hacer el SignInAsync. El resultado es:
public class CookieAuthenticationManager : IAuthenticationManager { private readonly IHttpContextAccessor _httpContextAccessor; private readonly PrincipalContext _principalContext; public CookieAuthenticationManager(IHttpContextAccessor accesor, PrincipalContext principalContext) { _httpContextAccessor = accesor; _principalContext = principalContext; } public async Task<bool> Authenticate(string username, string password) { try { if(_principalContext.ValidateCredentials(username, password)) { var oUsuarioActivo = UserPrincipal.FindByIdentity(_principalContext, IdentityType.SamAccountName, username); List<Claim> claimsUsuarioActivo = new List<Claim> { new Claim(ClaimTypes.Name,oUsuarioActivo.Name), new Claim(ClaimTypes.Sid,oUsuarioActivo.Sid.ToString()), new Claim(ClaimTypes.Email,oUsuarioActivo.EmailAddress) }; claimsUsuarioActivo.AddRange(oUsuarioActivo.GetGroups(). Select(x => new Claim(ClaimTypes.Role, x.SamAccountName))); ClaimsIdentity identity = new ClaimsIdentity(claimsUsuarioActivo, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { IssuedUtc = DateTime.UtcNow, ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30) }; await _httpContextAccessor.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), authProperties); return true; } else { return false; } } catch { return false; } } }
Configuramos todo en el Program.cs:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Configuración de las cookies builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme). AddCookie(options => { options.LoginPath = "/Login/Login"; } ); // Necesario para inyectar el HttpContext en clases. builder.Services.AddHttpContextAccessor(); // Nuestro servicio de autenticación builder.Services.AddScoped<IAuthenticationManager, CookieAuthenticationManager>(); // Objeto que nos da acceso al dominio builder.Services.AddScoped<PrincipalContext>(context => new PrincipalContext(ContextType.Domain, "deruy.com")); (...) app.UseAuthentication(); app.UseAuthorization(); (...)
Y listo. Añado algunos
[Authorize(Roles ="Sevilla")]
para probar y el prototipo está terminado y es funcional.