Autor en Google+
Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

10 años online 🎂

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 8 de mayo de 2012
ASP.NETImaginad que tenemos un sistema web de cierto volumen y decidimos estructurarlo en aplicaciones independientes, cada una publicada en un subdominio propio:
  • www.acme.org, que sería el sitio principal.
  • crm.acme.org, con el sistema CRM de la empresa.
  • erp.acme.org, con un sistema de gestión empresarial.
  • administration.acme.org con las herramientas de administración del sistema.
  • etc.
Desde un punto de vista operativo, es probable que nos interese suministrar un mecanismo de autenticación de usuarios compartido entre todas estas aplicaciones, de forma que el usuario, una vez identificado, pueda pasar de una a otra sin necesidad de introducir de nuevo sus credenciales.

No es una tarea complicada en ASP.NET, aunque hay que hacer algunos ajustillos para que todo funcione correctamente. Veámoslos.

Primero: ampliar el alcance de la cookie

Como sabemos, el procedimiento estándar de autenticación consiste en comprobar que las credenciales suministradas por un usuario son correctas (con membership o cualquier otro mecanismo), y en caso afirmativo, generar una cookie con información encriptada sobre el mismo. Esta cookie, llamada por defecto “.ASPXAUTH”, viaja en las sucesivas peticiones hacia el servidor, de forma que éste puede comprobar que el usuario ha sido autenticado satisfactoriamente con anterioridad.

El problema es que por defecto esta cookie es específica para cada host, por lo que sólo estará disponible para el dominio desde el que ha sido generada, con todos sus subdirectorios. Así, cuando se supera el procedimiento de autenticación y se crea la cookie (por ejemplo llamando a FormsAuthentication.SetAuthCookie()), lo que se enviará al servidor en los encabezados de la respuesta es lo siguiente:

Set-Cookie: .ASPXAUTH=F1E37685DF9CBED74094D02958BA239B4AEAA0BDEF2FF379A2E2C5A
                      4B7F9AC271B7F14BCFFE3E18799434EE8886CF4A0227E6BE92BC91E
                      34601D0FCC18D3F1786D5060329DB578DF2BB5148F6AB2972D72C3D
                      B17A437CE977660E552B92A6E5F981F3E6CE6037065244E1F0AB0BD
                      A570D61DEB02; path=/; HttpOnly


Pues bien, para conseguir nuestros objetivos, lo primero que nos interesa es asegurar que esta cookie estará disponible en los subdominios del dominio desde la cual se ha generado.

Afortunadamente podemos configurar bastantes aspectos del sistema de autenticación basado en formularios simplemente tocando un poco el web.config, y en este caso simplemente debemos indicar el dominio (de segundo nivel) para el cual queremos que la cookie sea válida, incluyendo sus subdominios. en el parámetro domain:
  <authentication mode="Forms">
    <forms loginUrl="~/Account/LogOn" timeout="2880" domain="acme.org" />      
  </authentication>
El valor de este parámetro será el nombre del dominio raíz del que colgarán todos los subdominios a los que haremos la cookie visible. Una vez introducido este valor en el web.config, la cookie de autorización será la siguiente:

Set-Cookie: .ASPXAUTH=4A67B460978D78217D52248318EB68717E857D7C08012057D0B6731
                      896E0E4EB9023AD2C02024CFB7D190617CCCD97FBA1E6ED484CE3FF
                      3581E7C8BE31FA204508F5ABB0DF994ADD698369132A5AF932AFF40
                      A267422C9ABCA86E620B9041ABB2E97C516880960F3D8193B209616
                      5978108AB500; domain=acme.org; path=/; HttpOnly


Este cambio tendremos que hacerlo únicamente en la aplicación que genere la cookie, es decir, aquella que incluya la implementación del procedimiento de autenticación del usuario. En nuestro caso, por ejemplo, podría ser la aplicación “raíz”, www.acme.org.

Segundo: encriptar la cookie usando una clave común

Esa secuencia de letras y números que veis en la cookie no es más que el resultado de encriptar la información que necesita ASP.NET para mantener información sobre el usuario autenticado. Esta encriptación se realiza utilizando un algoritmo y unas claves específicas para cada sitio web, que por defecto son generadas de forma automática.

Así, si queremos que distintos sitios web puedan desencriptar la cookie y acceder a su contenido, lo cual es fundamental para mantener el usuario conectado, debemos hacer que todos ellos compartan el algoritmo y clave de encriptación. Esto se hace desde el web.config estableciendo estos aspectos en la entrada <machineKey>:

...
<system.web>      
    <machineKey        <!-- ¡Ojo no copiar y pegar! -->
         validationKey="6171035B16CD1EE0E401BA3E7348DE49FA9FB9C043B3CDE3BA4FF
                        EDDC84B167C68B83916FAD8AEE4CFEE001AD5CEA8A4B3E28D51F9
                        D5EA55CD5F276E67B71FC6"
         decryptionKey="3F12339F897F687F4456FEC2446167C621BDE5F664178CBEF9AF0
                        40DB82EC806"
         validation="SHA1"
         decryption="AES"
    />
    ...
</system.web> 


Puedes generar este elemento usando el generador de MachineKey online, copiar el código y pegarlo en la sección <system.web> del web.config de todos los sitios web, raíz y subdominios, de la aplicación.

A partir de este momento ya podemos probar el sistema completo en funcionamiento. Si el mecanismo de autenticación se encuentra en www.acme.org, la autorización viajará al navegar por todos sus subdominios (crm.acme.org, erp.acme.org…), éstos serán capaces de desencriptar las credenciales y, por tanto, el usuario permanecerá logado en el sistema.

Tercero: ajustar Redirecciones

Dado que hemos considerado que todas las aplicaciones son privadas, con toda seguridad estarán protegidas contra accesos de usuarios no autenticados.Por ejemplo, en ASP.NET MVC probablemente tengamos todas las acciones protegidas con un filtro [Authorize], o en WebForms tendremos secciones <authorization> en el web.config para denegar el acceso a todas sus funcionalidades.

En cualquier caso, nos interesa que los usuarios no autenticados sean redirigidos a la aplicación desde la cual pueda autenticarse, por lo que podemos debemos indicar la URL de la página de login en el parámetro loginUrl como sigue:
    <authentication mode="Forms">
      <forms loginUrl="http://www.acme.org/account/logon" timeout="2880">
      </forms>
    </authentication>
Este cambio sería necesario hacerlo en todas las aplicaciones a las que no vamos a permitir el acceso anónimo (crm.acme.org, erp.acme.org, …), con lo que aseguramos que cualquier intento de entrada de usuarios no autenticados será redirigida al sitio principal, desde el cual podrá identificarse.

Cuarto: retocar la URL de retorno y procesarla tras la autenticación

Casi hemos acabado, pero aún hay un detalle cuya solución no es tan inmediata como los puntos anteriores.
El parámetro ReturnUrl
Cuando se produce la redirección hacia la página de login, automáticamente se añade a la petición un parámetro llamado ReturnUrl donde se almacena la URL a la que estaba intentado acceder el usuario. Esto permite devolverlo a ella una vez se haya autenticado en el sistema.

El problema es que este parámetro no incluye el host, por lo que en una aplicación distribuida en distintos dominios esta información se perderá. Es decir, si un usuario intenta acceder directamente a erp.acme.org/customers/index, será redirigido a la URL http://www.acme.org/account/logon?ReturnUrl=customers/index para ser autenticado, y cuando esto ocurra, el sistema no tiene información suficiente como para devolverlo a la página a la que deseaba ir en un principio (en erp.acme.org).

Lamentablemente no podemos “influir” en la forma en que ASP.NET genera el contenido para este parámetro al redirigir al usuario, por lo que o bien hacemos nosotros la redirección de forma manual al detectad que el usuario no se ha autenticado, o bien nos introducimos en algún punto avanzado del ciclo de ejecución de la petición para modificar el valor original del parámetro.

Un primer acercamiento de la implementación de esta última opción podría ser la siguiente, implementando el evento Application_EndRequest (en el global.asax.cs, válido tanto en MVC como en Webforms):
protected void Application_EndRequest(object sender, EventArgs e)
{
    if (Response.StatusCode == (int)HttpStatusCode.Found && !Request.IsAuthenticated)
    {
        string redirectUrl = this.Response.RedirectLocation;
        if (redirectUrl.Contains("ReturnUrl="))
        {
            var host = HttpUtility.UrlEncode(
                            "http://" +
                            this.Request.Url.Host +
                            (Request.Url.IsDefaultPort ? "" : ":" + Request.Url.Port));
            Response.RedirectLocation = redirectUrl.Replace("ReturnUrl=", "ReturnUrl=" + host);
        }
    }
}
Como podéis observar, simplemente intentamos detectar cuándo la respuesta enviada al cliente es una redirección, y si el usuario no está autenticado y existe un parámetro ReturnUrl, lo modificamos para que presente también el host (y puerto). De esta forma, ya llegará a la página de login la dirección completa a la que hay que enviar el usuario cuando supere la autenticación.

Ya llega la URL completa!

Y por último, ya lo único que quedaría sería implementar la redirección a la URL suministrada en el parámetro ReturnUrl una vez confirmada la identidad del usuario, es decir, tras establecer la cookie.

Eso sí, tened un poco de cuidado antes de redirigir y comprobad que la dirección a la que vais a enviarlo forma parte de vuestro sitio (por ejemplo, comprobando que el host de destino pertenece a acme.org) para evitar la vulnerabilidad Open Redirect.

Publicado en Variable not found.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

Aún no hay comentarios, ¡sé el primero!