Seguridad en Servicios Web SOAP ASP.NET - Token de acceso (I)
La autenticación de usuarios basada en Tokens, es posiblemente hoy en día la forma mas utilizada para aplicar seguridad a las infraestructuras de intercambio de datos basadas en Servicios Web.
En el ámbito de los servicios Web API Rest, disponemos de múltiples soluciones de seguridad basadas en Tokens (OAuth, JWT, etc.) que nos facilitan las tareas autenticación de usuarios. Sin embargo, en lo que se refiere a Servicios Web SOAP, las alternativas de seguridad basadas en Tokens no están tan consolidadas como en los servicios Web API Rest.
Para dar una solución a este inconveniente, en este artículo veremos como implementar un sistema sencillo pero efectivo de autenticación de usuarios basado en Tokens, para Web Services SOAP de ASP.NET.
Creando nuestro Web Service SOAP con Visual Studio
Para este ejemplo, utilizaremos Visual Studio 2019 con todas las actualizaciones necesarias para usar el SDK de .NET Framework 4.6.x.
En primer lugar, crearemos un nuevo proyecto del tipo Aplicación Web ASP.NET Vacía con el nombre MiApp.WebServices
.
Seguidamente, agregaremos al proyecto un nuevo elemento del tipo Servicio web (ASMX) con el nombre ClientesWS.asmx
. Este será el Web Service sobre el que desarrollaremos con posterioridad.
Los Modelos de datos
Antes de comenzar a desarrollar nuestro Web Service SOAP, debemos definir los Modelos de datos sobre los que trabajaremos posteriormente.
Primero crearemos una nueva clase llamada Cliente.cs
, la cual será la base para construir una lista de clientes que con posterioridad devolveremos desde el Servicio web a través del método GetClientes()
.
public class Cliente
{
public int IdCliente { get; set; }
public string NombreCliente { get; set; }
public string NombreEmpresa { get; set; }
public string Telefono { get; set; }
public string Email { get; set; }
}
También crearemos la clase TokenAcceso.cs
, la cual contendrá además del propio Token de acceso, los datos identificativos del usuario que lo solicita. Este modelo de datos, lo devolverá el método GetTokenAcceso()
.
public class TokenAcceso
{
public Guid Id { get; set; }
public string Nombre { get; set; }
public string Apellidos { get; set; }
public string Email { get; set; }
public string Rol { get; set; }
public Guid Token { get; set; }
}
Las cabeceras SOAP
Como ya seguramente sabemos, SOAP es un estándar que nos permite definir la estructura de un documento XML que será enviado o recibido por un Servicio Web por medio del protocolo HTTP.
La estructura de este documento XML consta básicamente de un cuerpo (Body), donde irán alojados los datos tanto de ida como vuelta, y una cabecera (Header), que también permite incluir datos en las solicitudes.
Siguiendo esta estructura, utilizaremos la cabecera (Header) para enviar al Web Service todos los datos referentes a la seguridad, ya sea tanto el usuario y contraseña de autenticación como el propio Token de acceso a los servicios.
Para ello, crearemos dos nuevas clases (CabeceraSoapAcceso.cs
y CabeceraSoapToken.cs
) que heredarán de la clase System.Web.Services.Protocols.SoapHeader
. Estas clases representarán las cabeceras SOAP que utilizaremos posteriormente para enviar al Servicio Web todos los datos relativos a la seguridad.
La cabecera de acceso: CabeceraSoapAcceso
// DEFINE UNA CABECERA SOAP PARA EL WEBMETHOD GetTokenAcceso()
// QUE INCLUIRÁ Usuario y Password DEL CLIENTE.
public class CabeceraSoapAcceso : System.Web.Services.Protocols.SoapHeader
{
public string Usuario { get; set; }
public string Password { get; set; }
public TokenAcceso ValidarUsuario(string usuario, string password)
{
// AQUI LA LÓGICA DE VALIDACIÓN //
// Simulamos la lógica de validación del usuario
if (usuario.ToLower() == "admin" && password.ToLower() == "admin")
{
// SI EL USUARIO ES VÁLIDO, DEVOLVEMOS UN OBJETO DEL TIPO TokenAcceso
// CON LOS DATOS DEL USUARIO REGISTRADO.
TokenAcceso tokenAcceso = new TokenAcceso()
{
Id = Guid.NewGuid(),
Nombre = "Rafael",
Apellidos = "Acosta",
Email = "mi.email@gmail.com",
Rol = "Administrador"
};
return tokenAcceso;
}
else
{
// SI NO ES VÁLIDO EL USUARIO RETORNAMOS NULL.
return null;
}
}
}
Como vemos en el código, esta cabecera SOAP consta de dos propiedades (Usuario
y Password
), las cuales servirán para autenticar el usuario en el sistema de información corporativo.
La autenticación la haremos mediante el método ValidarUsuario(string usuario, string password)
, el cual devolverá un objeto del tipo TokenAcceso
en el caso de ser un usuario válido, en caso contrario devolverá null.
La cabecera del Token: CabeceraSoapToken
// DEFINE UNA CABECERA SOAP PARA EL WEB SERVICE
// QUE INCLUIRÁ EL Token de Acceso DEL CLIENTE
public class CabeceraSoapToken : System.Web.Services.Protocols.SoapHeader
{
public string TokenAcceso { get; set; }
public bool ValidarTokenAcceso (string tokenAcceso)
{
// COMPRUEBA QUE EL TOKEN DE ACCESO RECIBIDO SEA EL MISMO
// QUE EL QUE SE ENCUENTRA EN LA CACHE DE LA PETICIÓN.
if (!string.IsNullOrEmpty(tokenAcceso))
{
TokenAcceso _tokenAcceso = HttpRuntime.Cache[tokenAcceso] as TokenAcceso;
if (_tokenAcceso?.Token.ToString() == tokenAcceso)
return true;
else
return false;
}
else
{
return false;
}
}
}
Esta cabecera SOAP será la encargada "transportar" el Token que dará acceso a los servicios, mediante la propiedad TokenAcceso
.
El método ValidarTokenAcceso (string tokenAcceso)
, será el encargado de comprobar que el Token recibido es un Token válido para acceder al Servicio Web.
Nota: La comprobación de la validez del Token recibido, se realizará comparándolo con el que se encuentra en la Cache Http del Servicio Web (
HttpRuntime.Cache
), y que ya fue anteriormente enviado al usuario cuando se validó en el sistema. Todo este proceso de generación del Token, almacenamiento en Cache y envío al usuario, lo veremos a continuación cuando construyamos el Web Service principal de la aplicación,ClientesWS.asmx.
Construyendo el Web Service
En este punto, ya estamos en disposición de comenzar a desarrollar el Servicio Web con seguridad basada en Tokens de nuestro ejemplo.
Para ello, abriremos el archivo ClientesWS.asmx
creado anteriormente, y le añadiremos el siguiente código:
/// <summary>
/// Descripción breve de ClientesWS
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// Para permitir que se llame a este servicio web desde un script, usando ASP.NET AJAX,
// quite la marca de comentario de la línea siguiente.
// [System.Web.Script.Services.ScriptService]
public class ClientesWS : System.Web.Services.WebService
{
private List<Cliente> clientes;
// OBJETOS DEL TIPO SoapHeader QUE SE ASIGNARÁN
// A CADA WebMethod DEL SERVICIO WEB.
public CabeceraSoapAcceso cabeceraSoapAcceso;
public CabeceraSoapToken cabeceraSoapToken;
public ClientesWS()
{
// SIMULAMOS UNA LISTA DEL MODELO DE DATOS.
clientes = new List<Cliente>()
{
new Cliente() { IdCliente = 1, NombreCliente = "Maria Anders", NombreEmpresa = "Alfreds Futterkiste", Telefono = "030-0074321", Email="maria.anders@northwind.com"},
new Cliente() { IdCliente = 2, NombreCliente = "Ana Trujillo", NombreEmpresa = "Ana Trujillo Emparedados y helados", Telefono = "(5) 555-4729", Email="ana.trujillo@northwind.com"},
new Cliente() { IdCliente = 3, NombreCliente = "Antonio Moreno", NombreEmpresa = "Antonio Moreno Taquería", Telefono = "(5) 555-3932", Email="antonio.moreno@northwind.com"},
new Cliente() { IdCliente = 4, NombreCliente = "Thomas Hardy", NombreEmpresa = "Berglunds snabbkop", Telefono = "(171) 555-7788", Email="thomas.hardy@northwind.com"},
new Cliente() { IdCliente = 5, NombreCliente = "Christina Berglund", NombreEmpresa = "Alfreds Futterkiste", Telefono = "0921-12 34 65", Email="christina.berglund@northwind.com1"},
new Cliente() { IdCliente = 6, NombreCliente = "Hanna Moos", NombreEmpresa = "Blauer See Delikatessen", Telefono = "0621-08460", Email="hanna.moos@northwind.com"},
new Cliente() { IdCliente = 7, NombreCliente = "Martín Sommer", NombreEmpresa = "Bólido Comidas preparadas", Telefono = "(91) 555 22 82", Email="martín.sommer@northwind.com"}
};
}
}
Como vemos, hemos creado dos cabeceras SOAP como las que ya vimos anteriormente, y hemos simulado en el constructor de la clase, una lista del tipo List<Cliente>
con datos de prueba.
Obteniendo el Token de acceso : GetTokenAcceso()
Seguidamente crearemos el método ([WebMethod]
) encargado de generar el Token de acceso, almacenarlo en Cache y enviarlo al usuario: GetTokenAcceso()
.
[WebMethod]
// INDICA QUE SE LE ASIGNARÁ A ESTE MÉTODO
// EL OBJETO cabeceraSoapAcceso COMO COMO CABECERA SOAP.
[SoapHeader("cabeceraSoapAcceso")]
public TokenAcceso GetTokenAcceso()
{
string usuario;
string password;
string token = string.Empty;
TokenAcceso tokenAcceso;
// COMPRUEBA SI EXISTE UNA CABECERA EN LA PETICIÓN,
// Y SI LA CABECERA CONTIENE EL USUARIO Y LA CONTRASEÑA
// DEL CLIENTE DE LA PETICIÓN.
if (cabeceraSoapAcceso == null ||
string.IsNullOrEmpty(cabeceraSoapAcceso?.Usuario) ||
string.IsNullOrEmpty(cabeceraSoapAcceso?.Password))
{
throw new SoapException("Acceso no autorizado", SoapException.ClientFaultCode,
new Exception(@"Se requiere Usuario y Contraseña en la Cabecera de la petición."));
}
else
{
// LA CABECERA DE LA PETICIÓN ES CORRECTA,
// OBTEBEMOS EL USUARIO Y LA CONTRASEÑA DEL CLIENTE.
usuario = cabeceraSoapAcceso.Usuario;
password = cabeceraSoapAcceso.Password;
}
// COMPRUEBA QUE EL USUARIO Y LA CONTRASEÑA
// DE LA PETICIÓN SON VÁLIDOS.
tokenAcceso = cabeceraSoapAcceso.ValidarUsuario(usuario, password);
if (tokenAcceso != null)
{
// EL USUARIO Y CONTRASEÑA SON CORRECTOS,
// SE CREA EL TOKEN DE ACCESO Y SE ALMACENA
// EN LA CACHÉ DE LA PETICIÓN ANTES DE DEVOLVERLO.
tokenAcceso.Token = Guid.NewGuid();
HttpRuntime.Cache.Add(
tokenAcceso.Token.ToString(),
tokenAcceso,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
TimeSpan.FromMinutes(2), // 2 MINUTOS DE VIDA (PARA PRUEBAS)
System.Web.Caching.CacheItemPriority.NotRemovable,
null
);
}
else
{
// EL USUARIO Y LA CONTRASEÑA NO SON VÁLIDOS.
throw new SoapException("Acceso no autorizado", SoapException.ClientFaultCode,
new Exception(@"EL Usuario y/o Contraseña no son válidos."));
}
// DEVOLVEMOS EL TOKEN DE ACCESO
if (!string.IsNullOrEmpty(tokenAcceso.Token.ToString()))
{
return tokenAcceso;
}
else
{
throw new SoapException("Acceso no autorizado", SoapException.ClientFaultCode,
new Exception(@"ERROR. No se ha podido generar el Token de acceso."));
}
}
Como vemos en el código, este método requiere de una cabecera SOAP del tipo CabeceraSoapAcceso
, donde irán el Usuario y Password para la autenticación.
Importante: La forma de asignar a un WebMethod una cabecera SOAP, se realiza mediante el atributo
[SoapHeader("...")]
. En este caso, le indicaremos que la cabecera SOAP será del tipoCabeceraSoapAcceso
mediante su objeto correspondiente:[SoapHeader("cabeceraSoapAcceso")]
.
Una vez comprobada la validez del usuario mediante los datos enviados en la cabecera (Usuario y Password), generamos un Token de acceso, lo añadimos al objeto del tipo TokenAcceso
, almacenamos este objeto en el HttpRuntime.Cache
y lo enviamos de vuelta al usuario.
Importante: Como vemos en la imagen, lo que almacenamos en Cache (
HttpRuntime.Cache
) es un elemento del tipo Key/Value. El Value contendrá un objeto del tipoTokenAcceso
con un tiempo de caducidad asignado, y el Key será el propio Token de acceso generado. Esto lo hacemos así, para poder mantener en Cache tantos objetos con Keys diferentes como usuarios accedan al Web Service, y cada uno de ellos con un tiempo de caducidad asignado.
Accediendo al Web Service : GetClientes()
En este punto, ya tenemos un Token de acceso válido al Web Service. Es el momento entonces de probar la funcionalidad de seguridad, creando un método ([WebMethod]
) en el servicio, que requiera este Token para acceder a sus datos.
Crearemos entonces el método GetClientes()
, que devolverá la lista de clientes (List<Cliente> clientes
), con los datos de prueba que creamos anteriormente en el constructor de Web Service.
[WebMethod]
// INDICA QUE SE LE ASIGNARÁ A ESTE MÉTODO
// EL OBJETO cabeceraSoapToken COMO COMO CABECERA SOAP.
[SoapHeader("cabeceraSoapToken")]
public List<Cliente> GetClientes()
{
// COMPRUEBA QUE EXISTA UNA CABECERA EN LA PETICIÓN.
if (cabeceraSoapToken == null || string.IsNullOrEmpty(cabeceraSoapToken.TokenAcceso))
{
throw new SoapException("Acceso no autorizado", SoapException.ClientFaultCode,
new Exception(@"Se requiere un Token de acceso en la cabecera de la petición.
Llamar primero al método GetTokenAcceso() para obtener el Token de acceso."));
}
else
{
// COMPROBAMOS QUE EL TOKEN DE ACCESO RECIBIDO
// EN LA CABECERA DE LA PETICIÓN, SEA EL MISMO
// QUE ESTÁ EN LA CACHÉ DE LA PETICIÓN
if (!cabeceraSoapToken.ValidarTokenAcceso(cabeceraSoapToken.TokenAcceso))
{
// SI EL TOKEN ES INVÁLIDO O NO EXISTE, DEVOLVEMOS EXCEPCIÓN.
throw new SoapException("Acceso no autorizado", SoapException.ClientFaultCode,
new Exception(@"El Token de acceso es incorrecto o ha caducado."));
}
else
{
// SI EL TOKEN ES VÁLIDO, DEVOLVEMOS LOS DATOS.
return clientes;
}
}
}
Como vemos en el código, este método requiere de una cabecera SOAP del tipo CabeceraSoapToken
, donde irá el Token de acceso que permitirá acceder a los datos.
El método ValidarTokenAcceso(cabeceraSoapToken.TokenAcceso)
de la propia cabecera SOAP, comprobará si el Token recibido es válido, comparándolo con el que se almacenó en su momento en la Cache del servicio (HttpRuntime.Cache
)
Como vemos en la imagen, cada Token de acceso solicitado por un usuario al Web Service, tiene su equivalente almacenado en la Cache del Servicio (HttpRuntime.Cache
).
Mientras exista un elemento del tipo Key/Value con un Token asignado en el HttpRuntime.Cache
, el usuario que lo solicitó, puede acceder al Servicio Web enviando en la cabecera de la solicitud este Token.
Por supuesto la validez del Token almacenado, depende de la caducidad que le hayamos asignado TimeSpan.FromMinutes(...)
.
HttpRuntime.Cache.Add(
tokenAcceso.Token.ToString(),
tokenAcceso,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
TimeSpan.FromMinutes(2), // 2 MINUTOS DE VIDA DESDE EL ÚLTIMO ACCESO (PARA PRUEBAS)
System.Web.Caching.CacheItemPriority.NotRemovable,
null
);
Importante: La caducidad del Token que le indicamos en el parámetro
TimeSpan slidingExpiration
(en este casoTimeSpan.FromMinutes(2)
), se refiere al tiempo que el Token estará en Caché desde el último acceso que tuvo, o sea, cada vez que accedamos a la Caché para comprobar que el Token es correcto, los 2 minutos de expiración vuelven a contar desde el principio. Viéndolo de otra manera, el tiempo de expiración, sería mas correcto llamarlo tiempo de inactividad.
Continuará ...
Como hemos visto, aplicar seguridad a Servicios Web SOAP mediante Tokens de acceso, es un proceso relativamente sencillo de implementar. Lógicamente y para completar este artículo, el siguiente paso será ver cómo consumir este Servicio Web desde una aplicación ASP.NET.
En el próximo artículo de este Blog Seguridad en Servicios Web SOAP ASP.NET - Token de acceso (II), veremos cómo consumir un Web Service con seguridad basada en Tokens de acceso desde una aplicación ASP.NET, teniendo en cuenta aspectos como el almacenamiento y recuperación del Token en el cliente, el tratamiento de excepciones SOAP, etc.
Nuevo comentario
Comentarios
EXCELENTE APORTE
Muy bueno e interesante!.
Enhorabuena!.
Hola Rafael.
Una forma peculiar y clara de explicar.
Felicidades!
¡Saludos!
Una gran aporte, pero estoy con un problema que tal vez tu puedas ayudarme a resolver, necesito consumir servicios con un token pero en c#.net 2005, ¿esta versión tiene las librerías para trabajar de esta manera?
Muchas gracias
Hola Rafael:
Muchas gracias por compartir tu conocimiento de forma sencilla y bien explicada.
Me alegra haber encontrado un blog que poder consultar para aprender nuevas cosas.
Saludos
@Israel:
Muchas gracias por tu comentario, espero que sigas visitando este Blog y compartiendo los artículos en las redes sociales y foros de programación.
Gracias, estoy diseñando un Web Service para varias aplicaciones, justo necesitaba este ejemplo.
Quiero consumirla desde una aplicación desktop y android.
Buen aporte.
Excelente artículo, muchas gracias.
@Miguel:
Muchas gracias por tu comentario y por seguir este Blog.
Estoy actualmente trabajando en la segunda parte de este artículo, y posiblemente estará disponible para la próxima semana.
Recuerda que compartir los artículos del Blog en redes sociales y hacer click en los banners de publicidad de vez en cuando, es fundamental para que este Blog siga ofreciendo publicaciones de calidad y en español.
Estimado Rafael,
esta información es muy útil para las personas que recién empiezan a estudiar los servicios web como yo, te agradecería mucho si es que tuvieras la segunda parte la publiques.
Nuevamente muchas gracias.
Saludos,
Miguel