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!

4 comentarios en “Access Control Service en Windows Azure Platform AppFabric”

  1. Hola Eber Irigoyen,

    Ya, lo sé =(
    Me puse en contacto con Rodrigo Corral, administrador de Geeks, para comentarle que estoy teniendo problemas con este asunto.
    Espero que para la próxima publicación quede solucionado.

    Perdonad las molestias.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *