Wiki

Integración de aplicaciones .NET al IDP

Introducción

El presente documento es una guía que describe cómo integrar una aplicación web desarrollada sobre la plataforma Microsoft .NET con la solución de SSO Web ofrecida como servicio por el estado uruguayo a todos sus organismos.

Esta guía permitirá, a un desarrollador con conocimientos en ASP.NET, modificar sus aplicaciones .NET existentes de forma de incorporar un esquema de autenticación federado en la misma, utilizando el estándar de federación de identidades SAML V2.0 (http://saml.xml.org/saml-specifications).

Una implementación de referencia puede encontrarse en github en el siguiente link: https://goo.gl/DzfFEH

Antecedentes

El estándar SAML se compone de varios capítulos, en los cuales se definen, entre otros elementos, los formatos que deben seguir las aserciones que son generadas por los miembros de una federación, y los protocolos de comunicación que deben utilizarse para implementar un esquema de autenticación federado.

Dentro de la federación, se definen dos roles fundamentales que participan en la misma:

  • Identity Provider : Es un servicio proveedor de identidades, centralizado, capaz de autenticar al usuario y generar una aserción firmada. Esta aserción es luego enviada al servicio que desea autenticar a sus usuarios de forma federada, el cual confía en el proveedor de identidades. El estado uruguayo provee un proveedor de identidades en el cual deberán confiar las aplicaciones que participen de la federación.
  • Service Provider : Es la aplicación (.NET en esta guía) que delega la autenticación de sus usuarios en el proveedor de identidades. El proveedor de identidades le indicará quién es el usuario autenticado, y la aplicación deberá confiar en esta información.

Esta guía indica los pasos a seguir para convertir una aplicación .NET en un Service Provider que autentique a sus usuarios con el Identity Provider del estado uruguayo, posibilitando de este modo la implementación de escenarios de Single Sign On entre aplicaciones del Estado, así como delegar los mecanismos de autenticación en un servicio que se ocupará de obtener las pruebas de identidad necesarias.

A continuación se incluye un diagrama indicando la interacción que deberá darse entre el Service Provider y el Identity Provider para la autenticación de un usuario. Este diagrama se realiza en base a un binding definido en el estándar SAML V2.0, denominado HTTP-POST. Si bien otros binings son también soportados por el estándar, este es el recomendado para la autenticación federada en la plataforma.

Requisitos

Los componentes utilizados en la presente guía requieren que la aplicación se encuentre desarrollada sobre la versión 3.5 del Microsoft .NET Framework, o superior. Asimismo, la misma deberá encontrarse publicada sobre HTTPS.

El Service Provider deberá contar con un certificado de servicio emitido por una CA autorizada, el cual deberá encontrarse instalado en el Certificate Store del equipo donde se encuentra la aplicación, incluyendo la clave privada. A los efectos de la presente guía, se asume que el certificado se encuentra instalado en el store Personal del equipo (LocalMachine / My). A continuación se incluye una captura del Certificate Store:

La cuenta configurada para la ejecución del sitio web deberá tener acceso a la clave privada del certificado para poder realizar la firma de lo SAML Requests enviados al IdP.

Aplicación .NET

Para la elaboración de la presente guía, se asume una aplicación .NET existente desarrollada sobre ASP.NET MVC 4, la cual cuenta con un único controlador (Home Controller) que despliega en la página de inicio un aviso de login.

Los conceptos detallados en esta guía aplican tanto a una aplicación MVC como WebForms, ya que se utilizan componentes de ASP.NET que son comunes a ambos frameworks.

En el diagrama anterior se pueden observar los componentes del sitio inicial, el cual cuenta con la siguiente página de inicio:

El código fuente de la solución de inicio se encuentra disponible junto con esta guía, de modo de permitir seguir los pasos detallados en la misma sobre esta aplicación de ejemplo.

Integración con el Identity Provider de la plataforma

1 – Incorporar componente SAML V2.0

El primer paso para integrar el proveedor de identidades de la plataforma, es utilizar el gestor de paquetes NuGet para agregar un componente al proyecto que permite trabajar con los protocolos definidos en SAML V2.0. A continuación se indica cómo realizar esta operación:

2 – Modificar archivo de configuración

Agregar las siguientes líneas de configuración en el web.config:

<configSections> 
	<section name="saml2" type="SAML2.Config.Saml2Section, SAML2" /> 
</configSections>

Reemplazar la sección <system.webServer> con el siguiente XML:

<system.webServer> 
	<validation validateIntegratedModeConfiguration="true" /> 
	<handlers> 
		<remove name="SAML2.Protocol.Saml20SignonHandler" /> 
		<remove name="SAML2.Protocol.Saml20LogoutHandler" /> 
		<remove name="SAML2.Protocol.Saml20MetadataHandler" /> 
		<add name="SAML2.Protocol.Saml20SignonHandler" verb="*" path="Login.ashx" type="SAML2.Protocol.Saml20SignonHandler, SAML2" /> 
		<add name="SAML2.Protocol.Saml20LogoutHandler" verb="*" path="Logout.ashx" type="SAML2.Protocol.Saml20LogoutHandler, SAML2" /> 
		<add name="SAML2.Protocol.Saml20MetadataHandler" verb="*" path="Metadata.ashx" type="SAML2.Protocol.Saml20MetadataHandler, SAML2" /> 
	</handlers> 
</system.webServer>

Agregar las siguientes líneas de configuración en el web.config, reemplazando los textos entre paréntesis rectos por los propios del servicio a integrar:

<saml2> 
	<allowedAudienceUris> 
		<audience uri="[Identificación servicio]" /> 
	</allowedAudienceUris> 
	<serviceProvider id="[Identificación servicio]" server="[Url del servicio]"> 
		<signingCertificate storeName="My" storeLocation="LocalMachine" findValue="[Subject del certificado a usar por el Servicio]" x509FindType="FindBySubjectName" /> 
		<endpoints> 
			<endpoint localPath="[URL Servicio]/Login.ashx" type="SignOn" redirectUrl="~/" /> 
			<endpoint localPath="[URL Servicio]/Logout.ashx" type="Logout" redirectUrl="~/" /> 
			<endpoint localPath="[URL Servicio]/Metadata.ashx" type="Metadata" /> 
		</endpoints> 
		<nameIdFormats allowCreate="true"> 
		<add format="urn:oasis:names:tc:SAML:1.1:nameid-format:transient"/> 
		</nameIdFormats> 
	</serviceProvider> 
	<identityProviders metadata="Metadata"> 
		<add id="[Entity ID del IdP]" default="true" omitAssertionSignatureCheck="true"> 
			<endpoints> 
				<endpoint type="Logout" url="[Endpoint de SLO del IdP]" binding="Redirect" /> 
			</endpoints> 
		</add> 
	</identityProviders> 
	<metadata> 
		<contacts> 
			<contact type="Administrative" company="" givenName="" surName="" email="" phone="" /> 
		</contacts> 
	</metadata> 
	<actions> 
		<clear/>
		<action name="SetSamlPrincipal" type="SAML2.Actions.SamlPrincipalAction, SAML2" /> 
		<action name="UIDParserAction" type="[Namespace del Sitio].UIDParserAction, [Nombre del sitio]" /> 
		<action name="Redirect" type="SAML2.Actions.RedirectAction, SAML2" /> 
	</actions> 
</saml2>

Modificar la sección “authentication” dentro de <system.web> para configurar autenticación usando SAML v2.0.

<authentication mode="Forms"> 
	<forms loginUrl="~/Login.ashx" timeout="2880" /> 
</authentication>

3 – Incorporar metadatos del servicio

Crear una carpeta dentro del proyecto denominada “Metadata”, e incluir dentro de la misma el archivo descriptor del Identity Provider, entregado por AGESIC (idp-metadata.xml).

4 – [MVC] Modificar rutas configuradas

Si se trata de un proyecto MVC, es necesario modificar las rutas para permitir acceder a los endpoints definidos para la comunicación utilizando SAML-P: Login.ashx, Logout.ashx y Metadata.ashx.

Incorporar las siguientes líneas en el archivo RouteConfig.cs dentro de la carpeta App_Start, al comienzo del método RegisterRoutes:

routes.IgnoreRoute("Login.ashx"); 
routes.IgnoreRoute("Logout.ashx"); 
routes.IgnoreRoute("Metadata.ashx");

5 – Incorporar action para la obtención del UID

A continuación se incluye el código fuente de una clase llamada UIDParserAction, que deberá incorporarse dentro de la carpeta App_Code del sitio, la cual permite obtener el nombre del usuario a partir del atributo SAML “UID”, e incorporarlo a la identidad de la sesión actual.

public class UIDParserAction : IAction 
{ 
	private string _name = "UIDParserAction"; private const string UID_ATTRIBUTE_NAME = "uid";
	public string Name 
	{ 
	get { return _name; } 
	set { _name = value; } 
	}
	public void SignOnAction(AbstractEndpointHandler handler, HttpContext context, Saml20Assertion assertion) 
	{ 
		if (!Saml20Identity.Current.HasAttribute(UID_ATTRIBUTE_NAME)) 
		{ 
		throw new ArgumentException("El SAML Assertion no contine un atributo uid."); 
		}
		if (!Saml20Identity.Current[UID_ATTRIBUTE_NAME].Any(x => x.AttributeValue.Length > 0)) 
		{ 
			throw new FormatException("El SAML assertion no contiene un valor para el atributo uid."); 
		}
		string uid = Saml20Identity.Current[UID_ATTRIBUTE_NAME].FirstOrDefault(x => x.AttributeValue.Length > 0).AttributeValue.FirstOrDefault();
		FormsAuthentication.SetAuthCookie(uid, false); 
	}
	public void LogoutAction(AbstractEndpointHandler handler, HttpContext context, bool IdPInitiated) 
	{ 
		FormsAuthentication.SignOut(); 
	} 
}

Esta clase es incorporada como Action en el archivo de configuración del punto 2. Es importante asegurar la coincidencia de los nombres para el correcto funcionamiento de la aplicación.

6 – Obtener metadatos del servicio local y enviar a AGESIC

Ejecutar la aplicación web. Desde la misma, se podrá acceder a la URL /Metadata.ashx, la cual generará un archivo descriptor xml que podrá ser descargado localmente, y enviado a AGESIC para que se configure el service provider dentro de la federación.

Mientras el SP no sea dado de alta, no será posible enviar SAML Requests al IdP, ya que el mismo rechazará las solicitudes.

7 – Acceder al servicio y probar la integración

Una vez que el SP fue dado de alta en el IdP, se podrá probar el Single Sign-On y Single Logout desde la misma. A continuación se incluyen capturas con ejemplos de estos flujos.

Al hacer click en “Iniciar sesión”, se reenviará automáticamente al IdP del Estado. Si el usuario se ya tenía una sesión abierta con el IdP, no se solicitará la re-autenticación del mismo, y será retornado a la pantalla de inicio con un SAML Assertion generado por el IdP. De lo contrario, se mostrará la siguiente pantalla:

Luego de autenticarse, se redirigirá al usuario automáticamente a la home del sitio con el Assertion generado por el IdP. A partir del mismo, se obtendrá el nombre de usuario dentro del atributo UID:

Nótese que para acceder al nombre de usuario, la aplicación utiliza el mecanismo habitual de ASP.NET (HttpContext.Current.User.Identity), con lo cual la autenticación por SAML es transparente para la aplicación. Los flujos posteriores de autorización serán los ya implementados por la aplicación, trabajando con el nombre de usuario retornado por el IdP.

El usuario podrá cerrar sesión desde la aplicación, lo cual envía un request de Single Logout al IdP, el cual cierra sus sesiones en todas las aplicaciones federadas donde cuenta con sesiones abiertas. Asimismo, el logout puede ser iniciado desde otra aplicación, generando los mismos resultados.

Anexo I - Diagnóstico de problemas

Si existieran problemas con la federación, es posible habilitar el logging de eventos para poder realizar el diagnóstico de los mismos. Este logging permitirá acceder a los mensajes enviados y recibidos con el IdP, así como eventos a internos de cada uno de los flujos.

1 – Crear clase EventLogger

Crear un archivo EventLogger.cs en la carpeta App_Code del sitio web, con el siguiente contenido:

public class EventLoggerFactory : ILoggerFactory 
{ 
	private static readonly EventLogger logger = new EventLogger();
	public IInternalLogger LoggerFor(Type type) 
	{ 
		return logger; 
	}
	public IInternalLogger LoggerFor(string keyName) 
	{ 
		return logger; 
	} 
}

public class EventLogger : IInternalLogger 
{ 
	private const string LOGGER = "SAML2";
	public EventLogger() 
	{ 
		if (!EventLog.SourceExists(LOGGER)) EventLog.CreateEventSource(LOGGER, "Application"); 
	}
	private void WriteLog(string severity, string message, Exception ex) 
	{ 
		string innerMessage = message; 
		if (ex != null) string.Concat(innerMessage, "\r\n", ex.ToString()); EventLog.WriteEntry(LOGGER, string.Format("{0} - {1} - {2}", severity, DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"), innerMessage)); 
	}
	public void Debug(object message, Exception exception) 
	{ 
		WriteLog("DEBUG", message.ToString(), exception); 
	}
	public void Debug(object message) 
	{ 
		WriteLog("DEBUG", message.ToString(), null); 
	}
	public void DebugFormat(string format, params object[] args) 
	{ 
		if (args.Length > 0) WriteLog("DEBUG", string.Format(format, args), null); 
	}
	public void Error(object message, Exception exception) 
	{
		WriteLog("ERROR", message.ToString(), exception); 
	}
	public void Error(object message) 
	{ 
		WriteLog("ERROR", message.ToString(), null); 
	}
	public void ErrorFormat(string format, params object[] args) 
	{ 
		if (args.Length > 0) WriteLog("ERROR", string.Format(format, args), null); 
	}
	public void Fatal(object message, Exception exception) 
	{ 
		WriteLog("FATAL", message.ToString(), exception); 
	}
	public void Fatal(object message) 
	{ 
		WriteLog("FATAL", message.ToString(), null); 
	}
	public void Info(object message, Exception exception) 
	{ 
		WriteLog("INFO", message.ToString(), exception); 
	}
	public void Info(object message) 
	{ 
		WriteLog("INFO", message.ToString(), null); 
	}
	public void InfoFormat(string format, params object[] args) 
	{ 
		if (args.Length > 0) WriteLog("INFO", string.Format(format, args), null); 
	}
	public bool IsDebugEnabled 
	{ 
		get { return true; } 
	}
	public bool IsErrorEnabled 
	{ 
		get { return true; } 
	}
	public bool IsFatalEnabled 
	{ 
		get { return true; } 
	}
	public bool IsInfoEnabled 
	{ 
		get { return true; } 
	}
	public bool IsWarnEnabled 
	{
		get { return true; } 
	}
	public void Warn(object message, Exception exception) 
	{ 
		WriteLog("WARN", message.ToString(), null); 
	}
	public void Warn(object message) 
	{ 
		WriteLog("WARN", message.ToString(), null); }
		public void WarnFormat(string format, params object[] args) { if (args.Length > 0) WriteLog("WARN", string.Format(format, args), null); 
	} 
}

2 – Configurar EventLogger en el web.config

Incorporar la siguiente línea dentro del tag <saml2> del archivo de configuración:

<logging loggingFactory="[Namespace del sitio].EventLoggerFactory, [Nombre del sitio]" />

3 – Acceder al Event Log

Luego de realizar esta configuración, se podrá acceder al EventLog de Windows (eventvwr.msc) para encontrar las entradas de log, dentro de “Application”.

16636 Accesos
Promedio (0 Votos)
Añadir comentarios