ASP Core — Autenticación por cookie y Active Directory

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.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.