Seguridad en Servicios Web SOAP ASP.NET - Token de acceso (II)
En el anterior artículo de este Blog Seguridad en Servicios Web SOAP ASP.NET - Token de acceso (I), vimos cómo implementar un sistema de autenticación de usuarios basado en Tokens, para Web Services SOAP de ASP.NET.
En esta ocasión, y para complementar el artículo anterior, veremos cómo consumir un Web Service SOAP con seguridad basada en Tokens de acceso desde una aplicación ASP.NET Web Forms, teniendo en cuenta aspectos como la serialización, encriptación y almacenamiento del Token en el cliente.
Creando nuestra aplicación Cliente ASP.NET 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 (.NET Framework) del tipo Web Forms, con el nombre MiApp.WebClient
.
A continuación, crearemos el Cliente o Proxy que consumirá el Servicio Web (ClientesWS.asmx
) creado anteriormente en el artículo Seguridad en Servicios Web SOAP ASP.NET - Token de acceso (I).
Esto lo haremos creando una nueva Referencia de servicio (Botón derecho sobre el Proyecto: Agregar > Referencia de servicio... ) al Web Service (por ejemplo: https://localhost:44394/ClientesWS.asmx
) con el Nombre ClientesWS
.
Configuraciones previas - Web.config
Como ya sabemos, el Servicio Web ClientesWS.asmx
sobre el cual trabajaremos, requiere de un Usuario y Contraseña para poder devolvernos el Token de acceso que nos permitirá acceder a sus recursos disponibles.
Es por esto que definiremos en el Web.config
de la aplicación este Usuario y Contraseña, para poder utilizarlos mas adelante.
Dentro de la etiqueta <system.web>
añadiremos el siguiente código:
<appSettings>
<add key="UsuarioWS" value="admin" />
<add key="PasswordWS" value="admin" />
</appSettings>
Por otra parte, también vamos a necesitar encriptar el Token de acceso que el Web Service nos devuelve. Esto lo haremos principalmente por cuestiones de seguridad, ya que almacenar en el Cliente esta información de manera visible, no es una buena práctica.
Para ello definiremos en el Web.config
un <machineKey />
personalizado dentro de la sección <system.web>
. Su uso lo veremos y explicaremos más adelante:
<!-- MACHINEKEY PARA ENCRIPTAR -->
<machineKey validationKey="57E8CA69ADAB0E32F79A74B01DA32B2E05DC6D166ED7AAF40F84991E5E9BF0BB8C9EE4D3BEE998F845FCEE64951160C5572ACB3F75A004A1EBB2779F286E52B8"
decryptionKey="77E8F78B0BB51A741031088D1EE9DC0195E3447D862BB73B18D32E086784DDE4" validation="SHA1" decryption="AES" />
Creando el diseño del formulario - Default.aspx
A continuación, crearemos el diseño de la página (Default.aspx
) que se encargará de interactuar con el Web Service para obtener el Token de acceso y mostrarnos los datos.
Básicamente consistirá en una tabla Html donde mostraremos los datos devueltos por el Web Service, y un par de botones para obtener el Token y actualizar los datos.
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="MiApp.WebClient._Default" %>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<div class="container">
<h4>
<label>Token: </label>
<span runat="server" id="idTokenLabel" class="label label-success"></span>
</h4>
<%--Control de servidor ListView--%>
<asp:ListView runat="server" ID="idListView"
ItemPlaceholderID="itemPlaceHolder"
OnItemDataBound="idListView_ItemDataBound">
<%--Plantilla de diseño de la tabla Html--%>
<LayoutTemplate>
<table id="ListViewTable" class="table table-bordered">
<%--Cabecera de la tabla Html--%>
<thead>
<tr>
<th scope="col">Nombre Cliente</th>
<th scope="col">Nombre Empresa</th>
<th scope="col">Email</th>
</tr>
</thead>
<%--Cuerpo de la tabla Html--%>
<tbody>
<%--Control de servidor PlaceHolder, elemento que
contendrá la plantilla <ItemTemplate>--%>
<asp:PlaceHolder runat="server" id="itemPlaceHolder">
</asp:PlaceHolder>
</tbody>
</table>
</LayoutTemplate>
<%--Plantilla de los elementos dinámicos de la tabla Html filas y columnas--%>
<ItemTemplate>
<tr>
<td runat="server" id="NombreCliente"></td>
<td runat="server" id="NombreEmpresa"></td>
<td runat="server" id="Email"></td>
</tr>
</ItemTemplate>
</asp:ListView>
<h4><span runat="server" id="idExcepcionLabel" class="label label-danger"></span></h4>
</div>
<div class="container">
<div class="row">
<asp:Button runat="server" ID="idActualizar" OnClick="idActualizar_Click" Text="Obtener datos" CssClass="btn btn-primary" />
<asp:Button runat="server" ID="idSolicitarToken" OnClick="idSolicitarToken_Click" Text="Solicitar Token" CssClass="btn btn-danger" />
</div>
</div>
</asp:Content>
El CodeBehind - Default.aspx.cs
En este punto, ya estaremos en disposición de crear nuestro Cliente Web que consumirá el Web Service. Básicamente, todo el desarrollo lo centraremos sobre la clase ClientesWS.ClientesWSSoapClient
, la cual nos dará acceso a los métodos del Servicio Web GetTokenAcceso()
y GetClientes()
.
El código C# para nuestra página Default.aspx
sería el siguiente:
public partial class _Default : Page
{
private ClientesWS.ClientesWSSoapClient _clientesWSClient;
private ClientesWS.TokenAcceso _tokenAccesoWS;
// RECUPERAMOS DEL web.config EL LOS DATOS DE ACCESO AL WEB SERVICE.
private string _usuarioWS = ConfigurationManager.AppSettings["UsuarioWS"];
private string _passwordWS = ConfigurationManager.AppSettings["PasswordWS"];
List<ClientesWS.Cliente> _clientes = new List<ClientesWS.Cliente>();
protected void Page_Load(object sender, EventArgs e)
{
idExcepcionLabel.Visible = false;
idActualizar.Visible = true;
idSolicitarToken.Visible = true;
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRenderComplete(e);
idListView.DataSource = _clientes;
idListView.DataBind();
}
protected void idListView_ItemDataBound(object sender, ListViewItemEventArgs e)
{
if (e.Item.ItemType == ListViewItemType.DataItem)
{
ClientesWS.Cliente cliente = (ClientesWS.Cliente)e.Item.DataItem;
((HtmlTableCell)e.Item.FindControl("NombreCliente")).InnerText = cliente.NombreCliente;
((HtmlTableCell)e.Item.FindControl("NombreEmpresa")).InnerText = cliente.NombreEmpresa;
((HtmlTableCell)e.Item.FindControl("Email")).InnerText = cliente.Email;
}
}
protected void idSolicitarToken_Click(object sender, EventArgs e)
{
// CREAMOS LA CABECERA SOAP CON LOS DATOS DE ACCESO
// PARA EL WEBMETHOD GetTokenAcceso()
ClientesWS.CabeceraSoapAcceso cabeceraSoapAcceso = new ClientesWS.CabeceraSoapAcceso();
cabeceraSoapAcceso.Usuario = _usuarioWS;
cabeceraSoapAcceso.Password = _passwordWS;
// SOLICITAMOS EL TOKEN DE ACCESO AL WEB SERVICE Y
// LO ALMACENAMOS EN UNA COOKIE
try
{
// SOLICITAMOS EL TOKEN DE ACCESO AL WEB SERVICE
_clientesWSClient = new ClientesWS.ClientesWSSoapClient();
_tokenAccesoWS = _clientesWSClient.GetTokenAcceso(cabeceraSoapAcceso);
idTokenLabel.InnerText = _tokenAccesoWS.Token.ToString() + " | " +
_tokenAccesoWS.Nombre + "" + _tokenAccesoWS.Apellidos + " | " +
_tokenAccesoWS.Email + " | " + _tokenAccesoWS.Rol;
// SERIALIZAMOS A JSON EL OBJETO DE TIPO ClientesWS.TokenAcceso
// PARA PODER ALMAZCENARLO EN UNA COOKIE.
var jsonTokenAccesoWS = JsonConvert.SerializeObject(_tokenAccesoWS);
// ENCRIPTAMOS EL OBJETO ClientesWS.TokenAcceso
var encTokenAccesoWS = Encriptar(jsonTokenAccesoWS);
// ALMACENAMOS EN UNA COOKIE EL OBJETO ClientesWS.TokenAcceso ENCRIPTADO
Response.SetCookie(new HttpCookie("TokenCookie", encTokenAccesoWS));
idActualizar.Visible = true;
idSolicitarToken.Visible = false;
}
catch (Exception ex)
{
idExcepcionLabel.Visible = true;
idExcepcionLabel.InnerText = ex.Message;
}
}
protected void idActualizar_Click(object sender, EventArgs e)
{
// COMPRUEBA SI EXISTE UNA COOKIE CON EL TOKEN DE ACCESO
if (Request.Cookies["TokenCookie"] != null)
{
// SI EXISTE, ACCEDEMOS AL WEB SERVICE
// OBTENEMOS EL VALOR DE LA COOKIE
var cookieValue = Request.Cookies["TokenCookie"].Value;
// DESENCRIPTAMOS EL VALOR DE LA COOKIE
var desTokenAccesoWS = Desencriptar(cookieValue);
// DESERIALIZAMOS EL VALOR DE LA COOKIE
// A UN OBJETO DEL TIPO ClientesWS.TokenAcceso
_tokenAccesoWS = JsonConvert.DeserializeObject<ClientesWS.TokenAcceso>(desTokenAccesoWS);
idTokenLabel.InnerText = _tokenAccesoWS.Token.ToString() + " | " +
_tokenAccesoWS.Nombre + "" + _tokenAccesoWS.Apellidos + " | " +
_tokenAccesoWS.Email + " | " + _tokenAccesoWS.Rol;
// CREAMOS LA CABECERA SOAP CON EL TOKEN DE ACCESO
// PARA EL WEBMETHOD GetClientes()
ClientesWS.CabeceraSoapToken cabeceraSoapToken = new ClientesWS.CabeceraSoapToken();
cabeceraSoapToken.TokenAcceso = _tokenAccesoWS.Token.ToString();
// ACCEDEMOS A LOS DATOS DEL WEB SERVICE CON EL TOKEN DE ACCESO.
try
{
_clientesWSClient = new ClientesWS.ClientesWSSoapClient();
_clientes = _clientesWSClient.GetClientes(cabeceraSoapToken).ToList();
idActualizar.Visible = true;
idSolicitarToken.Visible = false;
}
catch (Exception ex)
{
idExcepcionLabel.Visible = true;
idExcepcionLabel.InnerText = ex.Message;
idActualizar.Visible = false;
idSolicitarToken.Visible = true;
}
}
else
{
// SI NO EXISTE LA COOKIE ...
idTokenLabel.InnerText = string.Empty;
idActualizar.Visible = false;
idSolicitarToken.Visible = true;
}
}
private string Encriptar (string textoCookie)
{
if (!string.IsNullOrEmpty(textoCookie))
{
// Descomponemos texto de la cookie en bytes.
byte[] _textoCookie = Encoding.UTF8.GetBytes(textoCookie);
// Encriptamos el texto de la Cookie.
byte[] _TextoEncriptado = MachineKey.Protect(_textoCookie);
// Devolvemos el texto encriptado de la cookie.
return HttpServerUtility.UrlTokenEncode(_TextoEncriptado);
}
else
{
return string.Empty;
}
}
private string Desencriptar(string textoCookie)
{
if (!string.IsNullOrEmpty(textoCookie))
{
// Descomponemos valor de la cookie en bytes.
byte[] _textoCookie = HttpServerUtility.UrlTokenDecode(textoCookie);
// Desencriptamos el texto de la Cookie
byte[] _TextoDesencriptado = MachineKey.Unprotect(_textoCookie);
// Devolvemos el texto desencriptado de la cookie.
return Encoding.UTF8.GetString(_TextoDesencriptado);
}
else
{
return string.Empty;
}
}
}
El almacenamiento en Cliente del Token de acceso
Como vemos en el código, Cuando solicitamos el Token de acceso al Servicio Web mediante el método GetTokenAcceso(cabeceraSoapAcceso)
, este nos devuelve un objeto del tipo ClientesWS.TokenAcceso
el cual contiene además del propio Token, otra información adicional acerca del usuario que se validó en el sistema del información del Web Service.
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; }
// EL TOKEN DE ACCESO.
public Guid Token { get; set; }
}
Para conseguir la persistencia de este objeto (ClientesWS.TokenAcceso
) en el Cliente, lo que hacemos es almacenarlo en una Cookie del explorador, para poder utilizarlo posteriormente en las siguientes llamadas al Web Service.
Serializar y encriptar la información
Por cuestiones de seguridad, es fundamental que la información del Token de acceso que almacenamos en la Cooke, esté encriptada. Para ello lo que hacemos es serializar el objeto ClientesWS.TokenAcceso
a JSON:
// SERIALIZAMOS A JSON EL OBJETO DE TIPO ClientesWS.TokenAcceso
// PARA PODER ALMAZCENARLO EN UNA COOKIE.
var jsonTokenAccesoWS = JsonConvert.SerializeObject(_tokenAccesoWS);
y posteriormente lo encriptamos antes de almacenarlo en la Cookie. Para esto, hemos definido los métodos privados Encriptar(string textoCookie)
y Desencriptar(string textoCookie)
que vimos anteriormente en el código:
private string Encriptar (string textoCookie)
{
if (!string.IsNullOrEmpty(textoCookie))
{
// Descomponemos texto de la cookie en bytes.
byte[] _textoCookie = Encoding.UTF8.GetBytes(textoCookie);
// Encriptamos el texto de la Cookie.
byte[] _TextoEncriptado = MachineKey.Protect(_textoCookie);
// Devolvemos el texto encriptado de la cookie.
return HttpServerUtility.UrlTokenEncode(_TextoEncriptado);
}
else
{
return string.Empty;
}
}
private string Desencriptar(string textoCookie)
{
if (!string.IsNullOrEmpty(textoCookie))
{
// Descomponemos valor de la cookie en bytes.
byte[] _textoCookie = HttpServerUtility.UrlTokenDecode(textoCookie);
// Desencriptamos el texto de la Cookie
byte[] _TextoDesencriptado = MachineKey.Unprotect(_textoCookie);
// Devolvemos el texto desencriptado de la cookie.
return Encoding.UTF8.GetString(_TextoDesencriptado);
}
else
{
return string.Empty;
}
}
Importante: Es en este punto, donde vemos la utilidad del
<machineKey />
que definimos anteriormente en elWeb.Config
de la aplicación. Este<machineKey />
se utilizará como clave única para encriptar y desencriptar la Cookie con los métodosMachineKey.Protect(_textoCookie)
yMachineKey.Unprotect(_textoCookie)
. Para más información, pueden consultar el artículo Encriptar y desencriptar cookies en una aplicación ASP.NET.
Consideraciones finales
Como hemos visto en estos dos artículos, aplicar seguridad basada en Tokens de acceso a servicios Web SOAP de ASP.NET, es un proceso relativamente sencillo de implementar y de resultados bastante óptimos.
Por su puesto, este tutorial solo trata de dar una idea orientativa sobre la seguridad en los servicios Web SOAP. Cualquier sugerencia o mejora en la estructura, diseño e implementación, será bienvenida y contestada en la sección de comentarios de esta artículo.
Nuevo comentario
Comentarios
Quiero implementar un WEB Service SAOP que requiere una autentificación por un token, solo da esta referencia y no se como implementarlo:
Para el uso de este Token en los diferentes servicios de facturación, deberá incluir el mismo en el Header de la solicitud, incluimos el siguiente ejemplo en Java y en la aplicación SoapUI:
headers.put("apikey", Arrays.asList("TokenApi " + pToken));
Hola, como estas? me pareció de mucha ayuda tu ejemplo
de seguridad en servicios web, ahora me gustaria poder tener este mismo ejemplo en mi aplicación web, pero con vb.net. por si lo tienes y me lo pudieras compartir
Una vez más, decir que ha sido un gran trabajo.
Muy buen instructor, con ideas que cada uno puede tomar y ajustar a sus necesidades.
Gracias por la dedicación!.
@Irene:
Gracias por tu comentario.
En principio, si quieres utilizar un cliente VB.Net de escritorio (aplicación Windows Forms) para consumir el WebService, no vas a poder utilizar Cookies para almacenar el Token de acceso.
Las Cookies solo se almacenan en un explorador Web, por lo que en tu caso deberás almacenar el Token de acceso, por ejemplo, en el registro de Windows o en algún archivo temporal en el
FileSystem
.Por otra parte, si tu aplicación VB.Net de escritorio está utilizando un control del tipo WebBrowser para consumir el WebService, entonces si podrás utilizar Cookies para almacenar el Token.
Todo depende de tu implementación en cliente.
Hola Rafael! Muchas gracias por los artículos, son muy instructivos.
En esta propuesta que realizas, tanto el servidor como el cliente están basados en aplicaciones web con ASP. A mi me gustaría aplicar este sistema de autenticación, pero con el servidor como aplicación web con ASP y el cliente como aplicación de escritorio en VB.Net.
Sería posible adaptar la parte del cliente que hace uso de cookies para usarlo en Vb.Net? Si lo es, cómo habría que hacerlo?
Saludos, testeando, tengo dos consultas como realizar las pruebas en Postman o fiddler.
Se puede utilizar en cliente javascript (ajax). Es una de mis grandes dudas.
Excelente día y gracias. Saludos desde Ecuador
Rafael, nuevamente, ya encontre el error. Mala mia.
Muchisimas gracias por tu aporte!
Hola Rafael, como estas? estoy incursionando por el tema y me surgió un problemita con el ejemplo y no lo he podido solucionar. Cuando agrego la referencia al servicio me trae la clase "CabeceraSoapAcceso" , pero no la clase "CabeceraSoapToken". Podes orientarme cual puede ser el error. Esta realizado según el ejemplo. En el mismo tengo mas clases definidas, aunque no creo que este sea el problema.
Muchas gracias por tu tiempo.
Perfecto, completada la información y muy útil, una vez más muchas gracias.
@Julio:
Muchas gracias por tu comentario.
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.
Un saludo.
Excelente aporte, utilice SOAPUI para testear el WS.
Gracias.
Se puede hacer una prueba usando Postman u otro aplicativo online sin necesidad de crear un aplicativo?