El escenario es el siguiente: tenemos un conjunto de servicios WebApi que no tienen porque estar desplegados en Azure pero queremos que esos servicios solo sean accesibles para usuarios que se hayan autenticado previamente a través de un proveedor externo (p. ej. Twitter) usando la infrastructura de Azure Mobile Services.
Creación y configuración de Mobile Services
Esa es la parte fácil… Una vez hemos creado nuestro mobile service debemos irnos a la pestaña Identity y colocar los valores que nos pide según el proveedor de autenticación externo que queramos usar. En este ejemplo usaremos twitter así que nos pide el Api Key y el Api Secret:
Estos valores nos lo da twitter cuando creamos una aplicación en apps.twitter.com:
Otro aspecto a tener en cuenta es que twitter nos pide la URL de callback, esa debe ser la URL de nuestro mobile service añadiendo el sufijo /signin-twitter.
Pero bueno… todos esos detalles están explicados fenomenalmente en la propia ayuda de Azure.
Autenticarse usando twitter a través de Mobile Services (WAMS)
El cliente de Azure Mobile Services realiza un trabajo genial a la hora de gestionar todo el flujo oAuth para autenticar a usuarios usando cualquiera de los proveedores que soporta (Facebook, twitter, Google, una cuenta de Live ID). Si creas una aplicación Windows 8.1 o bien Windows Phone, autenticarse usando un proveedor externo (twitter) a través de Mobile Services es trivial:
- public async void PerformAuth()
- {
- MobileServiceClient client = new MobileServiceClient("https://beerlover.azure-mobile.net/");
- var user = await client.LoginAsync(MobileServiceAuthenticationProvider.Twitter);
- JwtToken = user.MobileServiceAuthenticationToken;
- }
Este código se encarga de gestionar todo el flujo OAuth y mostrar la UI correspondiente para que el usuario pueda introducir su login y password de twitter. Al final guarda en una propiedad JwtToken el token de autenticación retornado por WAMS.
En este punto finaliza nuestra interacción con mobile services: lo hemos usado para que el usuario se pudiese autenticar de forma fácil con un proveedor externo. Y al final obtenemos un token. Ese token no es de twitter, ese token es de WAMS y es un token JWT.
Json Web Token – JWT
JWT es un formato que define datos que puede incorporar un token que realmente es un objeto JSON. Es un formato que se está usando bastante ahora y tienes su especificación aquí.
Si ejecuto una aplicación que tenga el código anterior y me autentico usando twitter puedo ver como es el token JWT que me devuelve Mobile Services:
Bueno… es una ristra de carácteres bastante larga, pero básicamente se trata del objeto JSON codificado en Base64.
Para ver el contenido, puedes copiar el token y usar jwt.io para descodificar el token:
Podemos ver que hay tres colores en la imagen anterior y cada uno se corresponde a una parte. El primer color (verde) es un JSON con el siguiente formato:
- {
- "typ": "JWT",
- "alg": "HS256"
- }
Esto se conoce como JWT Envelope y nos indica el formato del token que viene a continuación (JWT) y el algoritmo usado para la firma digital (HS256).
La siguiente parte del token (azul) es realmente el JSON con los datos:
- {
- "iss": "urn:microsoft:windows-azure:zumo",
- "aud": "urn:microsoft:windows-azure:zumo",
- "nbf": 1418892674,
- "exp": 1421484674,
- "urn:microsoft:credentials": "{\"accessToken\":\"84274067-6C7zM6rnbL5VAIF8ARIZXWg6XTZ49x67klxRTAyIU\",\"accessTokenSecret\":\"fGDwXuJg7i8rJqwJFTki5EVbEiLvgx3MlCvunOaNOm81X\"}",
- "uid": "Twitter:84274067",
- "ver": "2"
- }
Realmente cada uno de esos campos es un claim (JWT está basado en claims).
De esos claims nos interesan realmente dos. El claim iss que contiene el issuer o quien ha generado el token y el claim aud que contiene la audience que indica para quien va dirigido el token. En este caso iss nos indica que el token ha sido generado por Mobile Services y el token aud nos indica que el token es para consumo de Mobile Services. Podemos ver más claims como uid donde hay un ID de usuario.
La última parte (en rojo en la imágen) es la firma digital del token. En este caso el Envelope nos indica que la firma digital es HS256 (HMAC usando SHA256) y eso nos sirve para comprobar que el token es válido.
Securizando nuestros servicios WebApi
La idea para securizar nuestros servicios WebApi es muy simple: A cada petición de nuestros servicios miraremos si se nos pasa el token JWT en la cabcera HTTP Authentication. Si recibimos un token JWT lo parsearemos y comprobaremos la firma digital (podríamos comprobar además todos los claims que queramos). Si la firma digital es válida, el token es válido y la petición se considera que proviene de un usuario de confianza.
Para validar el token JWT vamos a usar un DelegatingHandler. Ya expliqué en un post anterior sobre como securizar servicios WebApi lo que era un DelegatingHandler.
Para la validación del token JWT nos vamos a ayudar del paquete System.IdentityModel.Tokens.Jwt así que lo primero es agregarlo a la solución. Eso sí agrega la versión 3.0.0.0 mediante el comando:
Install-Package System.IdentityModel.Tokens.Jwt -Version 3.0.0.0
Esa versión es la misma que usa internamente Mobile Services para generar el token JWT.
Así lo primero es crearte una clase (yo la he llamado JwtDelegatingHandler) que derive de DelegatingHandler y redefinir el método SendAsync:
- protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- if (request.Method != HttpMethod.Options)
- {
- var tokenstr = RetrieveToken(request);
- if (tokenstr != null)
- {
- var handler = new JwtSecurityTokenHandler();
- if (handler.CanReadToken(tokenstr))
- {
- var token = handler.ReadToken(tokenstr);
- var secret = GetSigningKey(MasterKey);
- var validationParams = new TokenValidationParameters()
- {
- SigningToken = new BinarySecretSecurityToken(secret),
- AllowedAudience = "urn:microsoft:windows-azure:zumo",
- ValidIssuer = "urn:microsoft:windows-azure:zumo"
- };
- var principal = handler.ValidateToken(tokenstr, validationParams);
- Thread.CurrentPrincipal = principal;
- if (HttpContext.Current != null)
- {
- HttpContext.Current.User = principal;
- }
- }
- }
- }
- return base.SendAsync(request, cancellationToken);
- }
El método usa la clase JwtSecurityTokenHandler para mirar si el token JWT es válido sintácticamente y luego llama al método ValidateToken que devuelve un ClaimsPrincipal si el token es correcto. Dicho método valida la firma digital y también comprueba que el issuer y el audence del token sean correctos. Es por ello que los establecemos a los valores que coloca WAMS.
Finalmente si no salta ninguna excepción es que el token es correcto por lo que establecemos el ClaimsPrincipal devuelto como Principal del Thread actual de forma que el atributo [Authorize] entenderá que la petición está autenticada.
El método RetrieveToken obtiene el token JWT de la cabecera:
- private static string RetrieveToken(HttpRequestMessage request)
- {
- string token = null;
- IEnumerable<string> authzHeadersEnum;
- bool hasHeader = request.Headers.TryGetValues("Authorization", out authzHeadersEnum);
- if (!hasHeader)
- {
- return null;
- }
- var authzHeaders = authzHeadersEnum.ToList();
- if (authzHeaders.Count > 1)
- {
- return null;
- }
- var bearerToken = authzHeaders[0];
- token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
- return token;
- }
Y nos queda el método más importante el método GetSigninKey. Dicho método obtiene la clave que usó WAMS para firmar el mensaje y es la misma que debemos usar para comprobar la firma:
- internal static byte[] GetSigningKey(string secretKey)
- {
- var bytes = new UTF8Encoding(true, true).GetBytes(secretKey);
- using (SHA256Managed managed = new SHA256Managed())
- {
- return managed.ComputeHash(bytes);
- }
- }
El parámetro secretKey es la “Master Key” de WAMS que puedes ver en el portal de Azure en la opción de “Manage Keys”:
Nota: Si buscas por Internet verás que en muchos sitios agregan la cadena JWTSig a la Master Key. No lo hagas. No sé si en versiones anteriores de WAMS era necesario, pero ahora no lo es.
Con esto ya puedes usar [Authorize] para proteger tus servicios WebApi y que solo sean válidos para aquellas peticiones que tengan un token JWT que proviene de Windows Azure Mobile Services.
Usando System.IdentityModel.Tokens.Jwt 4.0.1
Hemos visto el código necesario para validar el token JWT de WAMS usando la versión 3.0.0.0 de System.IdentityModel.Tokens.Jwt que es la que usa internamente WAMS. Pero la última versión de este paquete (y la que os instalará NuGet si no indicáis versión) es, a la hora de escribir este post, la 4.0.1.
El código no es compatible ya que hay cambios en la clase JwtSecurityTokenHandler. Si usáis la versión 4.0.1 debéis usar el siguiente código en el DelegatingHandler:
- var validationParams = new TokenValidationParameters()
- {
- IssuerSigningToken = new BinarySecretSecurityToken(secret),
- ValidAudience = "urn:microsoft:windows-azure:zumo",
- ValidIssuer = "urn:microsoft:windows-azure:zumo"
- };
- SecurityToken outToken;
- var principal = handler.ValidateToken(tokenstr, validationParams, out outToken);
Este código es equivalente al anterior, podéis ver que los cambios básicos son nombres de propiedades y la firma de ValidateToken.
Usando Middleware OWIN
Comprobar que System.IdentityModel.Tokens.Jwt en su versión 4.0.1 era compatible con las firmas generadas por WAMS para los tokens JWT supone que podemos usar el middleware OWIN para validar el token JWT y olvidarnos del DelegatingHandler. Si la versión 4.0.1 no hubiese sido compatible (y algo de eso he leído en Internet, al menos en la versión 4.0.0) eso no sería posible ya que el middleware de OWIN depende de dicha versión (realmente la 4.0.0) para la validación de los tokens JWT.
Vale, para agregar el middleware OWIN basta con instalar el paquete Microsoft.Owin.Security.Jwt:
install-package Microsoft.Owin.Security.Jwt
Eso nos instalará las dependencias OWIN que tenemos, pero si no teníamos nada de OWIN instalada deberemos agregar el hosting de OWIN manualmente. Si después de este install-package no tenemos el paquete Microsoft.Owin.Host.SystemWeb deberemos instalarlo. Este paquete es el responsable de integrar el pipeline de OWIN y que se ejecute la clase de Owin Startup. El siguiente paso será crear una clase Startup:
- [assembly: OwinStartup(typeof(Beerlover.Server.Startup))]
- namespace Beerlover.Server
- {
- public class Startup
- {
- public void Configuration(IAppBuilder app)
- {
- var issuer = "urn:microsoft:windows-azure:zumo";
- var audience = "urn:microsoft:windows-azure:zumo";
- var secret = WebConfigurationManager.AppSettings["ClientSecret"];
- var signkey = GetSigningKey(secret);
- app.UseJwtBearerAuthentication(
- new JwtBearerAuthenticationOptions
- {
- AuthenticationMode = AuthenticationMode.Active,
- AllowedAudiences = new[] { audience },
- IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
- {
- new SymmetricKeyIssuerSecurityTokenProvider(issuer, signkey)
- },
- });
- }
- private byte[] GetSigningKey(string secret)
- {
- var bytes = new UTF8Encoding(true, true).GetBytes(secret);
- using (SHA256Managed managed = new SHA256Managed())
- {
- return managed.ComputeHash(bytes);
- }
- }
- }
- }
Dicha clase configura el middleware de OWIN para validar tokens JWT (a través del método UseJwtBearerAuthentication). De esta manera delegamos en OWIN la validación de los tokens JWT y ya no es necesario hacer nada en WebApi, es decir ya no debemos usar el DelegatingHandler. Por supuesto debemos seguir usando [Authorize] para indicar que servicios deben ser llamados de forma segura (con token JWT).
La solución usando OWIN es la recomendada ya que la validación de los tokens JWT se hace antes de que la petición llegue siquiera a WebApi y por lo tanto te permite integrarlo en otros middlewares (p. ej. si usas Nancy con OWIN este mismo código te sirve, mientras que el DelegatingHandler es algo propio de WebApi).
Enviando el token desde el cliente
Y simplemente por completitud del post, el código para enviar el token JWT desde el cliente sería el siguiente:
- protected void AddJwtToken(HttpClient client)
- {
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TwitterAuthService.Instance.JwtToken);
- }
Donde TwitterAuthService.Instance.JwtToken contiene el token devuelto por WAMS.
La posibilidad de delegar en WAMS todo el flujo oauth es muy cómoda y ello no nos impide que nuestra propia WebApi que puede estar en Azure (en un Website) o on-premise esté protegida a través del token JWT que emite WAMS. De esa manera la aplicación cliente tiene un solo token que usa tanto para acceder a los servicios WAMS (si los usase) como al resto de la API WebAPi que ni tiene que estar en Azure.
Espero que este post os sea de utilidad.
El escenario que vamos a abordar en este post es el siguiente: tienes una API creada con ASP.NET WebApi