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

Por Daniel Laco y Julieta Cozzi

Descargar ejemplos de este artículo (120 KB) Descargar ejemplos de este artículo (120 KB).

Contenido

 Introducción
 Autenticación de Usuarios por Roles
 Usuario No Autenticado
 Usuario Autenticado
 Conclusión

Introducción

Hoy en día, si existe 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 de 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 qué roles tienen permisos sobre qué recursos; qué 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 existen distintas formas de implementarlo. ¿Recuerdas cuando en ASP había que chequear la seguridad página por página?

Por supuesto que existen 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 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):

Bb972205.art206-img01-441x160(es-es,MSDN.10).gif
Figura 1: Ciclo de vida de páginas ASP .NET.Volver al texto.

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 demás 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 (Ver Figura 2):

  • Usuarios: 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: contiene todos los perfiles que se utilizarán en la aplicación y una descripción de los mismos.

  • Paginas: contiene todos los nombres y URLs de las páginas del sistema.

  • PerfilesPaginas: contiene todas las páginas que utiliza cada rol.

    Bb972205.art206-img02-348x234(es-es,MSDN.10).jpg
    Figura 2. Volver al texto.

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 también 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 está completa, solo se reprodujo parcialmente con el propósito de visualizar el objeto principal y cómo 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"] = ""; 
            } 

Sólo hemos reproducido el código correspondiente al método del acceso; la clase completa la puedes consultar en el código adjunto a esta 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, SecurityModules" name
 ="CustomAuthenticationModule" /> 
</ 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é 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 esté 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 permitan, por ejemplo, chequear a qué 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 cómo 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 realizar 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 estos de seguridad o no.

Bb972205.daniel_laco(es-es,MSDN.10).gif Daniel Laco trabaja como Arquitecto de Software y está a cargo de definir las estrategias para la incorporación de nuevas tecnologías y como Consultor en el desarrollo de soluciones Internet-Intranet. Dicta clases en la Universidad Tecnológica Nacional, Argentina, sobre programación avanzada y ha dictado numerosas conferencias técnicas y seminarios en diferentes universidades de ese país. Participa habitualmente como orador en eventos locales e internacionales organizados por Microsoft y grupos de usuarios. Ha participado realizando la programación de las pruebas de estrés y carga en el récord mundial de almacenamientos de datos sobre Exchange 2000, conjuntamente con la Universidad de Belgrano. Es Microsoft MVP desde 2001 y junto a Microsoft realizó la programación para el SQL Challenge.