Llamar a un Ajax.ActionLink desde JQuery con temporizador

Hace unos días me encontré con un caso en el que era necesario llamar de forma asíncrona a una acción de un controlador ASP.NET MVC, para refrescar una pantalla con los nuevos resultados. Además, era necesario que se hiciera cada X tiempo para que no me denegaran el servicio.
Desconozco si existe una forma más óptima de realizar esta funcionalidad en ASP.NET MVC pero, si sirve de ayuda, muestro el código de mi solución «temporal» a este caso en concreto:

  1. Creo un enlace con las propiedades de Ajax de la siguiente manera.
    <%=Ajax.ActionLink("Refresh","RefreshTags", new { tag = ViewData["tags"] }, new AjaxOptions { HttpMethod = "Post", OnBegin = "onBeginRetrievingTags", OnComplete = "onCompleteRetrivingTags", UpdateTargetId = "tweets" },new{Id="ActionLinkTags"})%>

    Para más información sobre Ajax con ASP.NET MVC, puedes consultar este otro post.

  2. Por el lado del controlador, creo la acción que va a devolver el listado actualizado, en este caso, de tweets.
    [HttpPost]
    public ActionResult RefreshTags(string tag)
    {
    ViewData["tags"] = tag;
    List<Tweet> entries = _serviceTweet.FilterBy(tag);
    return PartialView("TweetList", entries);
    }

    Si observamos detenidamente la acción, vemos que nos redirige a una vista parcial llamada TweetList donde muestro el resultado obtenido.

  3. Ejecutamos la aplicación, vemos que al renderizar la página en cuestión, nos devuelve el siguiente elemento para nuestro link.
    <a Id="ActionLinkTags" href="/Tweet/RefreshTags?tag=aspnetmvc" onclick="Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, httpMethod: 'Post', updateTargetId: 'tweets', onBegin: Function.createDelegate(this, onBeginRetrievingTags), onComplete: Function.createDelegate(this, onCompleteRetrivingTags) });">Refresh</a>
  4. Para llevar a cabo la llamada a través de JQuery, en un archivo js, he creado las siguientes funciones.
    function launchConfigTags() {

    $("#ActionLinkTags").dblclick(function(e) {

    Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(e),
    {
    insertionMode: Sys.Mvc.InsertionMode.replace,
    httpMethod: 'Post',
    updateTargetId: 'tweets',
    onBegin: Function.createDelegate(this, onBeginRetrievingTags),
    onComplete: Function.createDelegate(this, onCompleteRetrivingTags)
    });
    });

    Timer();
    }

    function Timer() { setTimeout("refreshTags()", 60000); }

    function refreshTags() { $("#ActionLinkTags").trigger("dblclick"); }

    De manera conjunta, realizan exactamente la misma llamada que si pulsaramos el link con un tiempo estimado de un minuto.

  5. Si queremos que el refresco se realice desde el primer momento que la página está disponible, podemos lanzar la función que bindea el evento doble click al link en el evento ready, iniciando de esta manera el ciclo de llamadas.
    $(document).ready(function() { launchConfigTags(); });

Espero que sea de utilidad.

¡Saludos!

Las tablas de Windows Azure Storage

En este post, voy a centrarme en las tablas de Windows Azure Storage, las cuales podemos definirlas como un conjunto de datos estructurados pero no relacionales. Es decir, tenemos un conjunto de registros con sus correspondientes columnas y primary keys pero no es posible la relación entre una tabla y otra del Storage. En el caso de necesitar tablas relacionales, tenemos SQL Azure.

Para comenzar, vamos a crear una pequeña aplicación donde almacenamos perfiles de usuario. Abrimos Visual Studio como administrador y creamos un nuevo proyecto de tipo Cloud Service con un Web Role.

Una vez creado, nos aparecerán dos proyectos: El web role y el proyecto de configuración de Windows Azure.

CREACIÓN DE LOS DATOS ESTRUCTURADOS

Como he mencionado antes, el contenido de las tablas de Storage son datos estructurados. Cada registro de esa tabla será una entidad y el conjunto de registros será una colección de entidades. En este caso, nuestra entidad se llamará Profile y estará compuesta por un nombre y un email. Para representar esta entidad/registro de nuestra tabla, creamos la siguiente clase en el proyecto ProfileManager:

using System;

namespace ProfileManager
{
public class Profile : Microsoft.WindowsAzure.StorageClient.TableServiceEntity
{
public Profile()
{
//PartitionKey y RowKey son requeridas para cada entidad añadida a la tabla
PartitionKey = "Users";
RowKey = string.Format("{0:10}_{1}", DateTime.MaxValue.Ticks - DateTime.Now.Ticks, Guid.NewGuid());
}

public string UserName { get; set; }
public string Email { get; set; }

}
}

Lo más importante a tener en cuenta en esta clase es que hereda de TableServiceEntity, que está dentro de una de las librerías de Windows Azure.
Hemos definido un constructor, donde inicializamos dos propiedades heredadas, PartitionKey y RowKey, las cuales son requeridas para poder insertar, modificar y eliminar correctamente una entidad en la tabla. De lo contrario, generaría una excepción.

Las propiedades heredadas por cada entidad son:

  • PartitionKey, se utiliza para determinar en qué partición debe estar la entidad que se está creando. Esto es debido a que las tablas están particionadas para soportar el balanceo de carga, a través de nodos de almacenamiento. Las entidades que pertenecen a una misma PartitionKey permanecen juntas. Además, esta clave forma parte de la primary key del registro. Para mejorar la escalabilidad de la aplicación, debemos tener en cuenta que se considera recomendable clasificar nuestros datos entre varias Partition Keys.
  • RowId, se trata de la segunda parte de la primary key. Es un identificador único dentro de la partición.
  • Timespan, es una propiedad de tipo DateTime que se utiliza para guardar la fecha de modificación de la entidad.

 

CONTEXTO DE LA TABLA

Por otro lado, necesitamos generar una serie de operaciones para que podamos trabajar con la tabla que, en un futuro, estará ubicada en la nube. Para ello, creamos una clase que herede de TableServiceContext, donde agregaremos las opciones de recuperar y añadir entidades en la tabla Profiles.

using System.Linq;
using Microsoft.WindowsAzure;

namespace ProfileManager
{
public class ProfileTableServiceContext : Microsoft.WindowsAzure.StorageClient.TableServiceContext
{
public ProfileTableServiceContext(string baseAddress, StorageCredentials credentials) : base(baseAddress, credentials) { }

public IQueryable<Profile> Profiles
{
get { return CreateQuery<Profile>("Profiles"); }
}

public void AddProfile(string userName, string email)
{
AddObject("Profiles", new Profile { UserName = userName, Email = email });
SaveChanges();
}
}
}

Lo primero que observamos es que recupera el constructor de la clase base. Es un paso obligatorio ya que la misma no contiene un constructor sin parámetros, lo cual es lógico, debido a que es necesario una dirección que nos indique dónde está el endpoint donde queremos operar y, como segundo parámetro, las credenciales para tener acceso.

Nota: Es necesario añadir la dll System.Data.Services.Client.

CADENA DE CONEXIÓN A DESARROLLO

Antes de realizar las pruebas oportunas contra la nube, podemos realizar un testeo inicial con el entorno de desarrollo Development Storage y Development Fabric. Para realizar las pruebas en local, creamos una entrada en la sección connectionStrings del archivo de configuración con el siguiente valor:

<connectionStrings>
<add name="AzureConnection" connectionString="UseDevelopmentStorage=true"/>
</connectionStrings>

CREACIÓN DE LA TABLA

Tanto en local como en la nube, antes de añadir registros, necesitamos crear la tabla en el Storage. Si bien es un paso que solamente debemos realizar una vez, podemos utilizar el archivo de inicio WebRole.cs que se generó cuando creamos el proyecto. Dentro del método OnStart(), añadimos el siguiente código:

using System.Configuration;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace ProfileManager
{
public class WebRole : RoleEntryPoint
{
public override bool OnStart()
{
DiagnosticMonitor.Start("DiagnosticsConnectionString");

// For information on handling configuration changes
// see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.
RoleEnvironment.Changing += RoleEnvironmentChanging;

CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) =>
configSetter(ConfigurationManager.ConnectionStrings[configName].ConnectionString));

var storageAccount = CloudStorageAccount.FromConfigurationSetting("AzureConnection");
CloudTableClient.CreateTablesFromModel(typeof(ProfileTableServiceContext),
storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);
return base.OnStart();
}

Lo primero que debemos especificar es de qué manera podemos recuperar la configuración necesaria para conectar a la cuenta de Storage a través de SetConfigurationSettingPublisher.
Una vez indicado el modo, recuperamos la cadena de conexión utilizando FromConfigurationSetting y, por último, creamos la tabla llamado a CreateTablesFromModel, pasándole como parámetros la clase que creamos con las operaciones que podemos realizar contra nuestra tabla, y que estaba heredando de TableServiceContext, el endpoint y las credenciales.

Para probar el ejemplo, he creado un pequeño formulario con dos textbox donde añadimos un nombre y un email y, pulsando en el botón Add Profile, añadimos esos datos en forma de entidad a la tabla del Storage y mostramos las entidades almacenadas hasta el momento.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="ProfileManager.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<div>
<asp:Label runat="server" ID="lblUserName" Text="UserName:"></asp:Label>
<asp:TextBox runat="server" ID="txtUserName"></asp:TextBox>
<asp:Label runat="server" ID="lblEmail" Text="Email:"></asp:Label>
<asp:TextBox runat="server" ID="txtEmail"></asp:TextBox>
<asp:Button runat="server" ID="btnAdd" Text="Add Profile" OnClick="btnAdd_Click" />
</div>
<div>
<asp:GridView ID="grvProfiles" runat="server" BackColor="White" BorderColor="#E7E7FF"
BorderStyle="None" BorderWidth="1px" CellPadding="3" GridLines="Horizontal">
<RowStyle BackColor="#E7E7FF" ForeColor="#4A3C8C" />
<FooterStyle BackColor="#B5C7DE" ForeColor="#4A3C8C" />
<PagerStyle BackColor="#E7E7FF" ForeColor="#4A3C8C" HorizontalAlign="Right" />
<SelectedRowStyle BackColor="#738A9C" Font-Bold="True" ForeColor="#F7F7F7" />
<HeaderStyle BackColor="#4A3C8C" Font-Bold="True" ForeColor="#F7F7F7" />
<AlternatingRowStyle BackColor="#F7F7F7" />
</asp:GridView>
</div>
</div>
</form>
</body>
</html>

using System;
using Microsoft.WindowsAzure;

namespace ProfileManager
{
public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

}

protected void btnAdd_Click(object sender, EventArgs e)
{
var storage = CloudStorageAccount.FromConfigurationSetting("AzureConnection");
var operations = new ProfileTableServiceContext(storage.TableEndpoint.ToString(), storage.Credentials);

operations.AddProfile(txtUserName.Text, txtEmail.Text);
grvProfiles.DataSource = operations.Profiles;
grvProfiles.DataBind();
}
}
}

 


CADENA DE CONEXIÓN PARA WINDOWS AZURE STORAGE

Cuando ya estemos listos para utilizar el Storage de Azure, necesitamos crear una cuenta de almacenamiento y  cambiar la cadena de conexión:

<connectionStrings>
<add name="AzureConnection" connectionString="DefaultEndpointsProtocol=https;AccountName=YOUR_ACCOUNTNAME;AccountKey=PRIMARY_ACCESS_KEY"/>
</connectionStrings>

Nota: Existen una serie de combinaciones para realizar la conexión pero, por el momento, usaremos la mencionada a modo introductorio.

Si arrancamos de nuevo la aplicación y añadimos un par de registros, obtendríamos el siguiente resultado.

CLOUD STORAGE STUDIO

Para finalizar me gustaría mencionar la siguiente aplicación, la cual es bastante útil para visualizar, crear, modificar y eliminar elementos del Storage de Azure (tanto tablas, como blobs y queues). Su nombre es Cloud Storage Studio y, si bien es de pago, tenemos la posibilidad de probarla de forma gratuita durante 30 días.
Si accedemos a la tabla que acabamos de generar con esta demo, podemos ver todos los detalle de la misma tal y como se muestra en la siguiente captura.

¡Saludos!

Creación de una nueva Storage Account en Windows Azure

Dentro de Windows Azure, no solamente podemos subir y consumir aplicaciones sino que, además, tenemos a nuestra disposición la parte de Storage
(almacenamiento). Dentro de Windows Storage se encuentran las
siguientes posibilidades:

  • Tablas: Se utilizan para el almacenamiento de datos estructurados pero no relacionales.
  • Queues: Las colas son útiles para almacenar mensajes en el orden en el que fueron recibidos para el procesamiento secuencial. Los objetos almacenados en Queue se insertan en un extremo y se quitan del otro.
  • Blobs: Sirven para el almacenamiento de archivos independientes.

Para utilizar cada uno de los apartados, primero debemos crear una nueva cuenta de almacenamiento, a través de los siguientes pasos:

1. Accedemos al portal de Windows Azure y seleccionamos Windows Azure y New Service.


2. En este apartado tenemos dos posibilidades: Storage Account y Hosted Services. Seleccionamos la primera de ellas.

3. Llegados a este punto, necesitamos poner un nombre al servicio y una descripción. Es importante tener en cuenta que este no es el nombre público sino más bien una forma de identificar esta cuenta de almacenamiento dentro de nuestra cuenta de Azure.

4. En este apartado debemos elegir qué nombre público deseamos que tenga este almacenamiento.

Para asegurarnos que está disponible, podemos pulsar sobre el botón Check Availability donde nos aparecerá un mensaje confirmando o denegando el nombre que acabamos de introducir.

En la parte inferior, tenemos un apartado muy importante: Dentro de la plataforma de Azure, podemos tener organizados nuestros servicios por grupos de afinidad. Todos aquellos servicios que permanezcan en el mismo grupo se encontrarán en la misma región. Si elegimos que no, nos permitirá seleccionar una región distinta. De lo contrario, podremos elegir uno de los grupos de afinidad que nos ofrece en la parte inferior, si ya tenemos alguno definido, o bien nos permitirá crear uno nuevo.

5. Por último, al pulsar sobre Create en la ventana anterior, nos mostrará un resumen de los datos relativos a la nueva cuenta de almacenamiento que acabamos crear.


Nota: CDN
Significa Content Delivery Network. El beneficio que conlleva es la mejora del rendimiento disminuyendo la latencia en la obtención de los datos del Storage, independientemente de la ubicación geográfica del usuario.

¡Saludos!

Subir una aplicación ASP.NET MVC a IIS 6

Cuando subimos una aplicación ASP.NET MVC a IIS 6 es muy posible que nos topemos con un error de tipo 404 si intentamos navegar por la aplicación:

Para poder solventarlo, necesitamos realizar una asignación de extensión para la aplicación (Application Extension Mapping) de la siguiente manera:

Accedemos al administrador de IIS y abrimos las propiedades de la aplicación ASP.NET MVC. Dentro de la pestaña Directorio virtual, pulsamos sobre el botón Configuración.


Pulsamos sobre el botón Insertar para asignar una extensión al directorio virtual.

Localizamos la dll aspnet_isapi.dll a través del botón Examinar, la cual está ubicada en WINDOWSMicrosoft.NETFrameworkv2.0.50727aspnet_isapi.dll

Deshabilitamos el campo «Comprobar si el archivo existe» y aceptamos.

Ahora todo debe ir sobre ruedas 😉

Para resolver el problema en otras versiones de IIS, podéis consultar el siguiente enlace (inglés): http://www.asp.net/learn/mvc/tutorial-08-cs.aspx

¡Saludos!

Subir una aplicación ASP.NET MVC a Windows Azure

Uno de los primeros artículos que publiqué acerca de Windows Azure fue la manera de subir una aplicación. Actualmente, en el caso de las aplicaciones ASP.NET MVC es necesario realizar una serie de ajustes para que pueda funcionar correctamente, ya que de no ser así no conseguirá arrancar en la nube.

En Visual Studio 2010 Beta 2 ya tenemos disponible la plantilla para añadir un proyecto de tipo ASP.NET MVC en un entorno de nube pero en el caso de Visual Studio 2008 no es así, aunque no es imposible 🙂

Creamos un proyecto de tipo Cloud Service.

No seleccionamos ninguna plantilla puesto que vamos a añadir un proyecto ya creado.

Añadimos un proyecto existente en ASP.NET MVC y, con el botón derecho, seleccionamos Unload Proyect.
Para que Windows Azure reconozca qué tipo de rol tiene este proyecto, debemos agregar la siguiente línea en el archivo csproj.

 

<RoleType>Web</RoleType>

El archivo debe quedar de la siguiente manera:

Volvemos a pulsar con el botón derecho sobre el proyecto para seleccionar Reload Project.
Por otro lado, debemos cambiar la propiedad Copy Local a true de tres dll:

 

Al haber añadido un proyecto a una solución Cloud Service ya generada, necesitamos enlazar ambos proyectos. Para ello, nos posicionamos en la carpeta Roles y con el botón derecho seleccionamos Add => Web Role Project in solution… y elegimos el proyecto ASP.NET MVC.

Una vez realizados estos pasos, podremos subir el proyecto a la nube sin problemas =)

¡Saludos!

Access Control Service en Windows Azure Platform AppFabric

Hace algún tiempo estuve hablando de qué era AppFabric, cuáles eran los componentes que lo formaban e incluso se mostró un pequeño ejemplo de la parte de Service Bus. Para cerrar la base de Windows Azure Platform AppFabric, hoy voy a centrarme en Access Control.

Access Control Service se encarga de la autenticación y autorización de los usuarios para nuestras aplicaciones desde la nube. En esta release inicial existe la posibilidad de autenticar a los clientes a través de claves simétricas, con nombre de usuario y contraseña, y además la autenticación de clientes a través de ADFS v2 (Active Directory Federation Services). En un futuro, esperan poder soportar un número mayor de tokens como Facebook, cuentas de Google, Windows Live ID entre otros.

Antes de comenzar he de decir que parte del código ha sido tomado de alguno de los ejemplos del SDK de AppFabric o bien del Training Kit de Azure como la validación de los tokens o la validación de la cabecera y he intentado simplificar los mismos lo más posible, además de la adicción de comentarios para facilitar la compresión de los ejemplos.

¿CÓMO FUNCIONA ACCESS CONTROL SERVICE?

1. CONFIGURACIÓN DE ACCESS CONTROL SERVICE

Existen tres conceptos en cuanto a configuración se refiere: Token Policy, Issuer y Scope.

Token Policy está compuesto por un ID, un nombre y el tiempo de expiración de los tokens. Se encarga de firmar los tokens que se emiten y establecer el tiempo de vida de los mismos.

I
ssuer es un recurso de Access Control que se utiliza para registrar clientes del servicio. Cada issuer está compuesto por un ID, un nombre y dos claves. Generalmente se utiliza un issuer por cliente y cada cliente tendrá una clave a través de la cual conseguirá el token necesario para poder validar la comunicación con el servicio.

Scope se utiliza para agrupar reglas dentro de Access Control Service, enlazando una politica de token y la dirección a la que aplica. Se identifica por un ID y un nombre. Cada regla indica el Issuer asociado y las operaciones que puede realizar dentro del scope.

Existen varias opciones para crear cada uno de los recursos en el Access Control del namespace: A través de líneas de comandos (PowerShell o CMD) o bien a través de una interfaz que nos facilitan en el Windows Azure Platform Training Kit.
Si optamos por la línea de comandos, debemos dirigirnos a la ubicación donde hemos instalado el SDK de AppFabric, donde estará disponible la herramienta Acm.exe y su archivo de configuración.

En primer lugar, abrimos el archivo de configuración para establecer el nombre del Service Namespace que debemos tener creado en la nube y su clave de administración:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="host" value="accesscontrol.windows.net"/>
<add key="service" value="Service Namespace"/>
<add key="mgmtkey" value="Current Management Key"/>
</appSettings>
</configuration>

Accedemos a la consola de comandos y nos posicionamos en la carpeta Tools del SDK. Los comandos para crear cada uno de los recursos serían los siguientes:

Token Policy

acm create tokenpolicy -name:returngis -autogeneratekey

El name corresponde al nombre descriptivo que queremos darle al Token Policy que vamos a generar y además indicamos que las claves serán autogeneradas.

Scope

acm create scope -name:returngisscope -appliesto:http://localhost/Hello -tokenpolicyid:TOKEN_POLICY_ID

Al igual que el comando anterior, se indica el nombre que queremos asociar al nuevo scope y appliesto corresponde con la URL donde se alojará el servicio que se beneficiará de la autenticación de Access Control.
Cuando se creó el Token Policy en el apartado anterior, la respuesta al finalizar el comando nos facilitó el ID del token policy generado. Para generar el Scope es necesario indicarle qué token policy tiene asociado pasandole como valor el ID.

Issuer

acm create issuer -name:returngisconsumer -issuername:returngisconsumer -autogeneratekey

En el caso de issuer, no está asociado directamente con ningún otro recurso y solamente es necesario etiquetar el issuer como tal y el nombre que queremos utilizar para las credenciales. Las contraseñas serán autogeneradas.

Rule

acm create rule -name:returngisconsumer -scopeid:ID_SCOPE -inclaimissuerid:ID_ISSUER -inclaimtype:Issuer -inclaimvalue:returngisconsumer -outclaimtype:action -outclaimvalue:sayHello

Por último, debemos crear una regla para el issuer y dentro del scope generados anteriormente. En este ejemplo, estamos creando una regla donde el usuario returngisconsumer puede reclamar una acción llamada sayHello.

Desde mi punto de vista, esta opción puede ser bastante pesada ya que debemos ir recuperando a mano cada uno de los ID que nos devuelve cada comando para poder asignarselo al comando oportuno. Por ello, como comentaba anteriormente, disponemos de una interfaz que nos ayuda a realizar estos pasos. Si bien es cierto que la misma tiene algunos defectos, es bastante más cómoda.
Necesitamos descargar el Training Kit, donde disponemos de una serie de ejemplos que además he necesitado para generar este mismo. El proyecto se llama AcmBrowser y está ubicado en WindowsAzurePlatformKitLabsIntroAppFabricAccessControlSourceAssetsAcmBrowser

Para poder ejecutar la aplicación es necesario acceder al proyecto, compilarlo y obtener el ejecutable para poder utilizarlo fuera de Visual Studio. De esta manera, si ejecutamos AcmBrowser.exe desde el raíz, obtendremos la siguiente interfaz:

Ya no es necesario modificar ningún archivo de configuración para añadir las credenciales que nos dan la posibilidad de administrar Access Control. En este entorno, debemos añadir las mismas cada vez que arranquemos la aplicación en los campos Service Namespace y Management Key.
Como opciones, podemos limpiar los valores introducidos en el Namespace, recuperarlos y guardarlos tanto en local como en la nube.

Nota:
En el caso del salvado, cada vez que pulsamos sobre esta acción intenta guardar cada uno de los valores que estamos visualizando en la interfaz, independientemente de si ya están incluidos en la nube o no. Por ello, recomiendo hacer una copia local, a través del disquete sin nube, y luego limpiar la nube y salvar de nuevo. De lo contrario, saltará una excepción 😉

Si pulsamos en la carpeta con la nube (Load from Cloud), y hemos realizado los comandos anteriores a través de Acm.exe, observaremos lo siguiente:

Como era de esperar, aparece cada uno de los recursos introducidos anteriormente agrupados por Issuers, Scopes y Token Policies.

ISSUERS

Si seleccionamosel issuer que creamos a través de la línea de comandos, podemos comprobar el nombre de issuer, el tipo de algoritmo utilizado (por defecto es Symmetric256BitKey) la clave actual y anterior. El cliente necesitará conocer tanto el Issuer Name como la Current Key para obtener un token válido.

TOKEN POLICIES

En este caso observamos que las únicas opciones disponibles son el tiempo de expiración de los tokens y regenerar la clave, además de facilitarnos la actual. Esta clave será necesaria para que el servicio pueda validar la petición del cliente.

Por último, podemos visualizar las opciones configuradas en la regla asociada al scope y al issuer que tenemos actualmente.

CREACIÓN DEL SERVIDOR

Para poder probar este ejemplo, he creado un servicio con un método de lo más sencillo para mostrar el modo de autentificación a través de los tokens que nos ofrece la nube. Es necesario abrir Visual Studio como administrador.

using System.ServiceModel;
namespace ServerACS
{
[ServiceContract]
public interface IService
{
[OperationContract]
string Hello(string name);
}
}

namespace ServerACS
{
public class Service : IService
{
public string Hello(string name)
{
return "Hello " + name;
}
}
}

Para crear el código de la consola, he tomado como referencia el proyecto WCFAuthorizationManager que nos ofrecen como ejemplo del SDK de AppFabric.

using System;
using System.ServiceModel;
using System.ServiceModel.Web;
using ServerACS.ValidationACS;

namespace ServerACS
{
class Program
{
static void Main()
{
const string serviceNamespace = "returngis";
const string tokenPolicyKey = "KEY=";
const string audience = "http://localhost/Hello";
const string claim = "action";
const string value = "sayHello";

var binding = new WebHttpBinding(WebHttpSecurityMode.None);
var address = new Uri(audience);

var host = new WebServiceHost(typeof(Service));
host.AddServiceEndpoint(typeof(IService), binding, address);

//Validación para los tokens, proporcionados por ACS, recibidos en las peticiones de los clientes
host.Authorization.ServiceAuthorizationManager = new AcsAuthorizationManager(
serviceNamespace,
audience,
Convert.FromBase64String(tokenPolicyKey),
claim,
value
);
host.Open();
Console.WriteLine("Service is listening....");
Console.ReadLine();
}
}
}

Para ver todo lo necesario a primera vista, no he realizado modificaciones en el web.config para poder ver claramente lo necesario del lado del servidor.
Lo más interesante es el apartado donde estamos creando un ServiceAuthorizationManager personalizado, facilitado también en el ejemplo del SDK, con algunas simplificaciones y comentarios añadidos para comprender qué hace en cada momento. Así pues recibe el service Namespace, la URL a la que aplica, la clave de token policy, el tipo que se reclama y el valor que contiene el tipo.

using System.Collections.Generic;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace ServerACS.ValidationACS
{
public class AcsAuthorizationManager : ServiceAuthorizationManager
{
private readonly TokenValidator _tokenValidator;
private readonly string _claimType;
private readonly string _claimValue;


public AcsAuthorizationManager(string serviceNamespace, string audience, byte[] tokenPolicyKey, string claimType, string claimValue)
{
_tokenValidator = new TokenValidator(serviceNamespace, audience, tokenPolicyKey);
_claimType = claimType;
_claimValue = claimValue;
}

protected override bool CheckAccessCore(OperationContext operationContext)
{
//Recuperamos las credenciales de la cabecera
string authorizationHeader = null;
if (WebOperationContext.Current != null)
{
authorizationHeader = WebOperationContext.Current.IncomingRequest.Headers[HttpRequestHeader.Authorization];
}
//Si no hay credenciales, no seguimos comprobando. La validación ha fallado.
if (string.IsNullOrEmpty(authorizationHeader)) return false;

//Si no comienza con WRAP (Web Resource Authorization Protocol) no es una cabecera válida.
if (!authorizationHeader.StartsWith("WRAP ")) return false;

//Separamos los elementos de la cabecera
string[] values = authorizationHeader.Substring("WRAP ".Length).Split(new[] { '=' }, 2);

//Si el número de partes es distinto de dos, no es válido.
if (values.Length != 2) return false;

//Si el primer valor no corresponde con access_token, tampoco es correcto.
if (values[0] != "access_token") return false;

//Si el segundo valor no está delimitado por , también sería incorrecto.
if (!values[1].StartsWith(""") || !values[1].EndsWith(""")) return false;

//Recuperamos el token
string token = values[1].Substring(1, values[1].Length - 2);

//Utilizamos el TokenValidator para comprobar el token
if (!_tokenValidator.Validate(token)) return false;

//Recuperamos la acción que se quiere realizar y el nombre de la acción
Dictionary<string, string> claims = _tokenValidator.GetNameValues(token);

//Se intenta recuperar el nombre de la acción que solicita el cliente (sayHello)
string actionValue;
if (!claims.TryGetValue(_claimType, out actionValue)) return false;

//Comprueba que el valor que se reclama es igual que el nombre de la acción que posee el servicio
if (!actionValue.Equals(_claimValue)) return false;

return true;
}
}
}

Como podemos ver, en esta clase se está heredando de ServiceAuthorizationManager y se sobreescribe el método CheckAccessCore para realizar toda la validación de la cabecera de autorización de la petición que se ha recibido. La mayoría de las comprobaciones son bastante simples, comprobando la estructura recibida.
Dentro de este código, debemos prestar especial atención al punto donde se valida el token a través de una instancia de la clase TokenValidation proporcionada también en los ejemplos del SDK.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace ServerACS.ValidationACS
{
public class TokenValidator
{
private const string IssuerLabel = "Issuer";
private const string ExpiresLabel = "ExpiresOn";
private const string AudienceLabel = "Audience";
private const string HmacSha256Label = "HMACSHA256";

private readonly byte[] _tokenKey;
private readonly string _trustedTokenIssuer;
private readonly Uri _trustedAudienceValue;

public TokenValidator(string serviceNamespace, string audience, byte[] tokenKey)
{
_tokenKey = tokenKey;
_trustedTokenIssuer = string.Format("https://{0}.accesscontrol.windows.net/", serviceNamespace.ToLowerInvariant());
_trustedAudienceValue = new Uri(audience);
}

public bool Validate(string token)
{
if (!IsHmacValid(token, _tokenKey))
{
return false;
}

if (IsExpired(token))
{
return false;
}

if (!IsIssuerTrusted(token))
{
return false;
}

if (!IsAudienceTrusted(token))
{
return false;
}

return true;
}

public Dictionary<string, string> GetNameValues(string token)
{
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException();
}

return
token
.Split('&')
.Aggregate(
new Dictionary<string, string>(),
(dict, rawNameValue) =>
{
if (rawNameValue == string.Empty)
{
return dict;
}

string[] nameValue = rawNameValue.Split('=');

if (nameValue.Length != 2)
{
throw new ArgumentException("Invalid formEncodedstring - contains a name/value pair missing an = character");
}

if (dict.ContainsKey(nameValue[0]))
{
throw new ArgumentException("Repeated name/value pair in form");
}

dict.Add(HttpUtility.UrlDecode(nameValue[0]), HttpUtility.UrlDecode(nameValue[1]));
return dict;
});
}

private static ulong GenerateTimeStamp()
{
// Default implementation of epoch time
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToUInt64(ts.TotalSeconds);
}

private bool IsAudienceTrusted(string token)
{
Dictionary<string, string> tokenValues = GetNameValues(token);

string audienceValue;

tokenValues.TryGetValue(AudienceLabel, out audienceValue);

if (!string.IsNullOrEmpty(audienceValue))
{
var audienceValueUri = new Uri(audienceValue);
if (audienceValueUri.Equals(_trustedAudienceValue))
{
return true;
}
}

return false;
}

private bool IsIssuerTrusted(string token)
{
Dictionary<string, string> tokenValues = GetNameValues(token);

string issuerName;

tokenValues.TryGetValue(IssuerLabel, out issuerName);

if (!string.IsNullOrEmpty(issuerName))
{
if (issuerName.Equals(_trustedTokenIssuer))
{
return true;
}
}

return false;
}

private static bool IsHmacValid(string swt, byte[] sha256HmacKey)
{
string[] swtWithSignature = swt.Split(new[] { "&" + HmacSha256Label + "=" }, StringSplitOptions.None);

if ((swtWithSignature == null) || (swtWithSignature.Length != 2))
{
return false;
}

var hmac = new HMACSHA256(sha256HmacKey);

byte[] locallyGeneratedSignatureInBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(swtWithSignature[0]));

string locallyGeneratedSignature = HttpUtility.UrlEncode(Convert.ToBase64String(locallyGeneratedSignatureInBytes));

return locallyGeneratedSignature == swtWithSignature[1];
}

private bool IsExpired(string swt)
{
try
{
Dictionary<string, string> nameValues = GetNameValues(swt);
string expiresOnValue = nameValues[ExpiresLabel];
ulong expiresOn = Convert.ToUInt64(expiresOnValue);
ulong currentTime = Convert.ToUInt64(GenerateTimeStamp());

if (currentTime > expiresOn)
{
return true;
}

return false;
}
catch (KeyNotFoundException)
{
throw new ArgumentException();
}
}
}
}


A través de la clave del Token Policy, el token facilitado por el cliente, la dirección del service namespace y la uri sobre la que aplica nuestra política es capaz de determinar si es un token válido a través de una serie de filtros.
Cuando un cliente haga una petición, en primer lugar se comprobará la cabecera de la misma, a través del Token Validator se testeará su veracidad y, en caso de ser un token válido, realizará la llamada al método solicitado por el cliente.

CREACIÓN DEL CLIENTE

Como pudimos ver al comienzo de este post, el cliente debe realizar también dos pasos para poder realizar una comunicación con éxito. En primer lugar, debemos solicitar un token proporcionado por el Access Control y, si la obtención del token se hace con éxito, podremos montar la petición para ser enviada al servicio.

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Web;
using ServerACS;

namespace Client
{
class Program
{
private const string ServiceName = "returngis";
private const string IssuerKey = "YOUR ISSUER KEY";
private const string AcsBaseAddress = "accesscontrol.windows.net";

static void Main()
{
Console.WriteLine("Enter your name:");
string name = Console.ReadLine();
string token = GetToken();

var binding = new WebHttpBinding(WebHttpSecurityMode.None);
var uri = new Uri("http://localhost/Hello");

var webChannelFactory = new WebChannelFactory<IService>(binding, uri);

var channel = webChannelFactory.CreateChannel();

//Este es el formato que debe seguir la cabecera para
//que el servidor lo reconozca como un token válido
string header = string.Format("WRAP access_token="{0}"", HttpUtility.UrlDecode(token));

using (new OperationContextScope(channel as IContextChannel))
{
WebOperationContext.Current.OutgoingRequest.Headers.Add("authorization", header);

Console.WriteLine(channel.Hello(name));
}
Console.WriteLine("Presh Enter");
Console.ReadLine();

webChannelFactory.Close();
}

private static string GetToken()
{
var client = new WebClient
{
BaseAddress = string.Format("https://{0}.{1}", ServiceName, AcsBaseAddress)
};

var values = new NameValueCollection
{
{"wrap_name", "returngisconsumer"},
{"wrap_password", IssuerKey},
{"wrap_scope", "http://localhost/Hello/"}
};

byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values);

string response = Encoding.UTF8.GetString(responseBytes);

Console.WriteLine("nToken From Access Control Service:{0}n", response);

return response
.Split('&')
.Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase))
.Split('=')[1];

}
}
}

La primera acción que realiza el cliente, nada más recuperar el texto introducido por consola, es intentar recuperar el token a través del método GetToken. En él creamos un objeto de tipo Webclient para realizar una llamada a través de POST con las credenciales generadas cuando creamos el recurso Issuer returngisconsumer. Si obtenemos un token con éxito, se mostrará por pantalla y lo retornaremos a Main.

Tanto la clave de Token Policy como de Issuer podemos recupearlas a través de la consola al finalizar la creación de los recursos o a través de la interfaz de AcmBrowser.

Facilito el ejemplo con los comentarios y algo más simplificados por si fuera de utilidad.

¡Saludos!