Autenticación de usuarios basada en Roles utilizando HTTPModules en ASP.NET

por Daniel Laco  23. septiembre 2007

Introducción   Código de Ejemplo HTTPMudules

Hoy en día, si hay algo crítico en todo sistema informático es el tema de la seguridad, particularmente en las aplicaciones Web, donde nos enfrentamos a una gran cantidad de ataques, de los mas diversos tipos.
Una de las necesidades más comunes en estos sistemas, además de los mecanismos propios de seguridad, es poder contar con un mecanismo de autorización basado en Roles o Perfiles, que permita al sistema verificar que roles tienen permiso a que recursos, que opciones de menú configurar o qué información mostrar en tiempo de ejecución.
En esta nota, explicaremos una implementación de este modelo de seguridad, utilizando la infraestructura que nos brinda ASP.NET, mediante la utilización de HttpModules, validando los Roles y Usuarios almacenados en una base de Datos.

Autenticación de usuarios por Roles

El trabajo con roles no es un tema nuevo en el desarrollo de aplicaciones, y hay distintas formas de implementarlo. Recuerdan cuando en ASP había que chequear la seguridad página por página?
Por supuesto que hay varias alternativas para tratar el mismo problema, algunas más eficientes y escalables que otras.
En ASP.NET esto está resuelto, ya que en el Web.Config se pueden configurar los accesos a las diferentes secciones de nuestra aplicación, por ejemplo, en la siguiente porción de código se indica que todos los usuarios tienen acceso a "página.aspx":

<location path="pagina.aspx">
    <system.web>
        <authorization>
            <allow users="*" />
        </authorization>
    </system.web>    
</location>

Pero, hay ocasiones en donde el escenario que se presenta es distinto, siendo necesario poder configurar dinámicamente el acceso a las páginas. Los usuarios, roles y páginas cambian continuamente por programación, en general, porque se almacenan en una base de datos, en un servicio de directorio como Active Directory o en alguna otra opción de almacenamiento.

Para atender a estos casos veremos una implementación de seguridad basada en HttpModules de ASP.NET, donde los roles (o perfiles) de los usuarios están almacenados en una base de datos, y la autorización es administrada con esta solución.

Primero debemos entender cómo funcionan y qué son estos HttpModules, para esto veamos el ciclo de vida de las páginas en ASP.NET y de qué modo interactúan estos módulos con el ciclo de vida de estas páginas :

El ciclo de una llamada en HTTP comienza en el IIS (Internet Information Server), continúa cuando la petición es enviada a aspnet_isapi.dll que es la que administra todo el flujo entre IIS y ASP.NET.
Cuando la petición ingresa en el circuito propiamente de ASP.NET, ésta pasa a través de los diferentes HttpModules configurados en la cadena, y por último la petición llega al HttpHandler.
Este HttpHandler se ocupará del armado de la página y realizará la escritura del Html hacia el IIS, que a su vez la retorna al navegador que realizó la petición original. (Ver figura 1):



Figura 1. Ciclo de vida de páginas ASP.NET

 

Dentro de este proceso, los HttpModules son filtros que se pueden programar e incluir en la configuración de la aplicación ASP.NET. Estos filtros permiten "atender" diferentes eventos que dispara ASP.NET durante el proceso de cada petición. Si miramos en el archivo machine.config de .NET veremos en la sección de HttpModules todos los filtros que vienen programados y configurados cuando se instala .NET, entre ellos están, por ejemplo, los que manejan las diferentes configuraciones de seguridad.

      <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
      <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
      <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" />
…..
      <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" />

Programar un HttpModule es sencillo, solo requiere implementar la interfaz IHttpModule y desde el código definir en cuáles eventos se va a responder. Una vez que el componente está programado, solamente es necesario agregar la entrada correspondiente en el .config de la aplicación. Más adelante trataremos este punto con mayor claridad.
Si con .NET tenemos la posibilidad de poder "engancharnos" en el ciclo de vida de una página, es por de mas interesante que utilicemos esta funcionalidad para nuestra solución de seguridad basada en roles.
Veamos ahora el ejemplo para entender la solución:

Para comenzar tenemos una base de datos con los usuarios que acceden a nuestra aplicación, los roles de los mismos y las páginas del sistema.

Es decir, contaremos con una estructura mínima de 4 tablas, a saber:

  • Usuarios, que contiene todos los datos de los usuarios necesarios según las reglas del negocio de la aplicación y el identificador del rol al que pertenecen.
  • Perfiles, que contiene todos los perfiles que se utilizarán en la aplicación y una descripción de los mismos.
  • Paginas, que contiene todos los nombres y urls de las páginas del sistema.
  • PerfilesPaginas, que contiene todas las páginas que utiliza cada rol. 

 

 

Necesitamos además una clase que contenga la información del usuario y el componente que cumpla la función de HttpModule.
El método de autenticación estará basado en Forms. Ahora bien, de qué forma podemos tener información del usuario autenticado en cualquier punto de la aplicación? Una de las alternativas podría ser guardar información en la Session del usuario. En este ejemplo reemplazaremos directamente el Principal del HttpContext por una clase propia que implementa la interfaz IPrincipal, además de tener las propiedades propias de la interfaz, agregaremos métodos y propiedades útiles para nuestro modelo de seguridad.

Al personalizar este objeto a nuestras necesidades, nos quedará lo siguiente:

  • Principal: es un objeto que contiene información asociada al usuario actual, por ejemplo, su rol y su identidad, así como otros datos que puedan ser de interés según las reglas del negocio.
  • Identidad (Identity): representa al usuario, tiene distintas propiedades que permiten obtener diferentes datos de los usuarios, por ejemplo:
System.Security.Principal.IPrincipal user;
string username = user.Identity.Name ;

Nos permite obtener el nombre del usuario autenticado en nuestro sistema.

  • Roles o Perfiles: simplemente son los nombres de los distintos roles que se agregan al objeto principal, los mismos pueden encontrarse en la base de datos.
    public class FormsPrincipal :IPrincipal, IMyAppPrincipal
    {
        private IIdentity _identity;
        private string [] _roles;
        private string _Perfil;        
 
        public FormsPrincipal( IIdentity identity, string [] roles)
        {
            _identity = identity;
            _roles = roles;
            _Perfil = Perfil;
        }
        
        //...    
 
        //Propiedad que utilizaremos para saber si el usuario tiene o no habilitado
        //el acceso a una determinada página
        public bool IsPageEnabled(string pageName)
        {
            return Perfiles.IsPageEnabled( pageName, this._Perfil );  
        }
 
        //...    
    }

Cabe destacar que esta clase no esta completa, solo se reprodujo parcialmente, con el propósito de visualizar el objeto principal y como hemos implementado la interfaz IPrincipal.

El paso siguiente es configurar y programar la autenticación basada en Forms:
Agregamos la entrada en el web.config

<authentication mode="Forms"> 
    <forms loginUrl="Login.aspx" timeout="20"/>
</authentication>

Y programamos nuestra página Login.aspx, hemos agregado los comentarios correspondientes a los efectos de clarificar la funcionalidad de cada línea de programación:

private void btnSubmit_Click(object sender, System.EventArgs e)
{
    string user = txtUser.Text;
    string password = txtPassword.Text;
 
    //Chequeo de usuario y contraseña
    SeguridadEnAspNet.Usuario oUser = new SeguridadEnAspNet.Usuario();
    string perfil = oUser.GetPerfil(user, password);
 
    if (perfil.Length > 0) // perfil vacío significa que no fue encontrado
    {
        //Invoca a componente que se encarga del Cache de los datos
        //en este caso de las páginas a las que el perfil tiene acceso
        SeguridadEnAspNet.UserCache.AddPaginasToCache( perfil, SeguridadEnAspNet.Perfiles.GetPaginas(perfil) ,System.Web.HttpContext.Current ); 
        
        // Crea un ticket de Autenticación de forma manual, 
        // donde guardaremos información que nos interesa
        FormsAuthenticationTicket authTicket = 
                new FormsAuthenticationTicket(2,  // version
                user,
                DateTime.Now, 
                DateTime.Now.AddMinutes(60),
                false, 
                perfil, // guardo el perfil del usuario
                FormsAuthentication.FormsCookiePath);
        // Encripto el Ticket.
        string crypTicket = FormsAuthentication.Encrypt(authTicket);
            
        // Creo la Cookie
        HttpCookie authCookie = 
                new HttpCookie(FormsAuthentication.FormsCookieName,
                crypTicket);
 
        Response.Cookies.Add(authCookie); 
 
        // Redirecciono al Usuario - Importante!! no usar el RedirectFromLoginPage
        // Para que se puedan usar las Cookies de los HttpModules
        Response.Redirect( FormsAuthentication.GetRedirectUrl(user,false));
    }
    else
        // Muestro mensaje de error
        tblWarning.Style["display"] = "";
}

Solo hemos reproducido el código correspondiente al método del acceso, la clase completa la pueden ver en el código adjunto a la nota.

Veamos ahora la programación de nuestro HttpModule.

 
using System;
using System.Web;
using System.Security.Principal;
using System.Web.Security;
 
namespace SeguridadEnAspNet
{
    /// <summary>
    /// Modulo de Administración de la Seguridad 
    /// Seguridad basada en Forms
    /// </summary>
    public class CustomAuthenticationModule : IHttpModule
    {
        public CustomAuthenticationModule()
        {}
 
        /// <summary>
        /// Inicializa el HTTPModule y asigna los EventHandlers a cada Evento
        /// Esta es la parte donde se define a que eventos va a atender el HttpModule
        /// </summary>
        /// <param name="oHttpApp"></param>
        public void Init(HttpApplication oHttpApp)
        {
            // Se Registran los Manejadores de Evento que nos interesa
            oHttpApp.AuthorizeRequest += new EventHandler(this.AuthorizaRequest);
            oHttpApp.AuthenticateRequest += new EventHandler(this.AuthenticateRequest);
        }
 
        public void Dispose()
        {}
 
        /// <summary>
        /// Administra la autorización por Request
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AuthorizaRequest( object sender, EventArgs e)
        {    
            if (HttpContext.Current.User != null)
            {
                //Si el usuario esta Autenticado
                if (HttpContext.Current.User.Identity.IsAuthenticated)
                {
                    if (HttpContext.Current.User is MyApp.Seguridad.FormsPrincipal)
                    {
                        MyApp.Seguridad.FormsPrincipal principal = (SeguridadEnAspNet.FormsPrincipal) HttpContext.Current.User;
                        //Se verifica si el Perfil del usuario tiene autorización para acceder a la página
                        if( !principal.IsPageEnabled( HttpContext.Current.Request.Path) )
                            HttpContext.Current.Server.Transfer( "NoAutorizado.aspx"); 
                    }
                }
            }
        }
 
        /// <summary>
        /// Autentica en Cada Request
        /// </summary>
        /// <param name="sender">HttpApplication</param>
        /// <param name="e"></param>
        private void AuthenticateRequest(object sender, EventArgs e)
        {
            if (HttpContext.Current.User != null)
            {
                //Si el usuario esta Autenticado
                if (HttpContext.Current.User.Identity.IsAuthenticated)
                {
                    if (HttpContext.Current.User.Identity is FormsIdentity)
                    {
                        //Traigo el Rol que esta guardado en una Cookie encriptada
                        FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
                        FormsAuthenticationTicket ticket = id.Ticket;
 
                        string cookieName = System.Web.Security.FormsAuthentication.FormsCookieName;
 
                        string userData = System.Web.HttpContext.Current.Request.Cookies[cookieName].Value;
 
                        ticket  = FormsAuthentication.Decrypt(userData);
 
                        string rol="";
                        if( userData.Length > 0 )
                            rol= ticket.UserData;
 
                        //Se crea la clase Principal  y se asigna al CurrenUser del Contexto                                    
                        HttpContext.Current.User = new SeguridadEnAspNet.FormsPrincipal(_identity, perfil);                    
                    }
                }
            }
        }//AuthenticateRequest
        
    } //class
 
} //namespace

Una vez que tenemos nuestro módulo de seguridad (HttpModule: CustomAuthenticationModule), se deberá referenciar en el archivo de configuración de nuestra aplicación como se muestra a continuación:

<authentication mode="Forms"> 
    <forms loginUrl="Login.aspx" timeout="20"/>
</authentication>
<authorization>
    <deny users="?" />  
</authorization>
//…
<!-- Modulo de Autorización -->
<httpModules>
<add type="SeguridadEnAspNet.CustomAuthenticationModule,  name="CustomAuthenticationModule"/>
    SecurityModules"
</httpModules>

De esta forma estamos configurando el modo de autenticación por formularios, indicándole que la página de autenticación es "Login.aspx" y que los usuarios anónimos no tienen acceso a nuestra aplicación. También estamos definiendo el HttpModule que utilizaremos, en este caso, para administrar la seguridad de la aplicación.

Con esta configuración, ¿qué es lo que ocurre cuando un usuario realiza la petición de una página de nuestra aplicación?

Usuario No Autenticado
Si el usuario no está autenticado aún, realiza el proceso normal de autenticación por Formularios (Forms).

Usuario Autenticado
Si el usuario está autenticado, el primer evento que se dispara es el AuthenticateRequest, para obtener los roles (o perfiles) del usuario desde una cookie, y con esta información creamos la clase FormsPrincipal. Este objeto será posteriormente asignado al HttpContext.CurrentUser.
El evento que se dispara a continuación es el AuthorizaRequest. Lo que hace es verificar si el usuario tiene acceso a la página que está solicitando, en caso de que no este autorizado, la aplicación lo derivará a una página de error con el mensaje correspondiente.
Si bien la funcionalidad que estamos mostrando se aplica para páginas, también se puede extender a la clase que implementa IPrincipal y agregarle otros métodos que permita por ejemplo chequear a que datos de una página tiene acceso el rol. Para esto, en cualquier parte de nuestro código se puede hacer lo siguiente:

SeguridadEnAspNet.CustomPrincipal  user = (SeguridadEnAspNet.CustomPrincipal) HttpContent.CurrentUser;
    If( ! user.IsDataVisible("txtNombreControl") )
        txtNombreControl.Visible = false;

Conclusión:
Hemos visto como podemos hacer una administración de seguridad de una aplicación, mediante la utilización de código más eficiente y elegante. En el ejemplo mostrado con solo cambiar el HttpModule, se puede hacer que una aplicación pase de autenticar y autorizar desde una base de datos, a una autorización basada en Active Directory.
Si bien en este caso con los HttpModules presentamos un escenario de seguridad, es importante tener en cuenta que esta funcionalidad de poder interceptar los diferentes pasos por los que transita ASP.NET, puede aplicarse a un sin número de otros escenarios, sean de seguridad o no.

Se pueden bajar el código de la nota aquí Código de Ejemplo HTTPModules

Tags:

ASP.NET

Acerca de los Autores

Este es el blog del equipo de VEMN SA 
Presentaremos temas que nos parezcan de interés sobre tecnología .NET, Procesos y Metodologías y todo aquello relacionado con el proceso de desarrollo de Software

Month List

BlogRoll

Download OPML file OPML