- Cómo securizar aplicaciones web usando ACS y tokens JWT.
- Desplegar aplicaciones web en Windows Azure WebSites que hagan uso de WIF.
- Cómo securizar servicios WebAPI usando ACS y tokens JWT.
- Cómo securizar una aplicación MVC que contenga tanto aplicaciones web como servicios WebAPI.
- Cómo securizar aplicaciones web usando Windows Azure Active Directory ( WAAD ).
- Cómo hacer uso del tenant de WAAD de Office 365 para securizar aplicaciones web
En el primer post de la serie veíamos cómo es posible securizar una aplicación web ASP.NET MVC como ACS y token JWT. En este post haremos un ejemplo similar, pero securizando un servicio ASP.NET WebAPI al cuál queremos llamar desde un cliente de forma segura.
Una vez creada, veremos cómo ésta aplicación tiene unos controladores de ejemplo que podemos usar para el ejemplo que estamos mostrando, ya que lo que nos importante en este caso es cómo securizar el acceso.
Con el proyecto de ejemplo, si ejecutamos la aplicación con F5 podemos acceder a los controladores WebAPI a través del protocolo GET desde el propio navegador y ver los resultados que devuelve. Al no tener seguridad cualquiera puede consultarlos.
Una vez que tenemos la aplicación de ejemplo, como en caso anterior, será necesario crear un namespace de ACS así como una relaying party con las URLs dónde estamos desplegando el servicio WebAPI en local.
Para seguir con el ejempo, el siguiente paso será añadir una referencia a System.IdentityModel, así como instalar a través de Nuget “JSON Web Token Handler For the Microsoft .Net Framework 4.5”, paquete que va a proporcionarnos diferentes clases para el manejo de tokens JWT.
Una vez realizado estos pasos, la principal diferencia con el ejemplo de MVC es que aquí no podremos hacer uso de la herramienta “Identity and Access…” que veíamos en el post anterior y la cuál nos permitía configurar nuestra aplicación para hacer uso de ACS.
En este caso tendremos que desarrollar nuestro propio DelegatingHandler para que poder validar en todas las peticiones que se realicen al servicio WebAPI que el cliente está debidamente autenticado contra el proveedor de identidad y STS que tengamos configurado en nuestro ACS.
A través del Global.asax añadiremos nuestro validador personalizado, el cuál se encarga de asegurarse de que el token se envían en las cabeceras de todas las peticiones, que el token es correcto y es establecer la identidad para que desde los controladores WebAPI pueda acceder a toda la información del usuario autenticado, por ejemplo, los claims.
GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
El código de nuestro handler sería el siguiente:
internal class TokenValidationHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { HttpStatusCode statusCode; string token; if (!TryRetrieveToken(request, out token)) { statusCode = HttpStatusCode.Unauthorized; return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode)); } try { // Use JWTSecurityTokenHandler to validate the JWT token JWTSecurityTokenHandler tokenHandler = new JWTSecurityTokenHandler(); List<string> issuers = new List<string>(); issuers.AddRange(ConfigurationManager.AppSettings["Issuers"].Split(new[] { ',' })); // Set the expected properties of the JWT token in the TokenValidationParameters TokenValidationParameters validationParameters = new TokenValidationParameters() { AllowedAudience = ConfigurationManager.AppSettings["AllowedAudience"], ValidIssuers = issuers, // Fetch the signing token from the FederationMetadata document of the tenant. SigningToken = new X509SecurityToken(new X509Certificate2(GetSigningCertificate(ConfigurationManager.AppSettings["ida:FederationMetadataLocation"]))) }; Thread.CurrentPrincipal = tokenHandler.ValidateToken(token, validationParameters); HttpContext.Current.User = Thread.CurrentPrincipal; return base.SendAsync(request, cancellationToken); } catch (SecurityTokenValidationException) { statusCode = HttpStatusCode.Unauthorized; } catch (Exception) { statusCode = HttpStatusCode.InternalServerError; } return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode)); } // This function retrieves ACS token (in format of OAuth 2.0 Bearer Token type) from // the Authorization header in the incoming HTTP request from the ShipperClient. private static bool TryRetrieveToken(HttpRequestMessage request, out string token) { token = null; IEnumerable<string> authzHeaders; if (!request.Headers.TryGetValues("Authorization", out authzHeaders) || authzHeaders.Count() > 1) { // Fail if no Authorization header or more than one Authorization headers // are found in the HTTP request return false; } // Remove the bearer token scheme prefix and return the rest as ACS token var bearerToken = authzHeaders.ElementAt(0); token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken; token = bearerToken.StartsWith("Authorization Bearer ") ? bearerToken.Substring(21) : bearerToken; return true; } public static byte[] GetSigningCertificate(string metadataAddress) { if (metadataAddress == null) { throw new ArgumentNullException(metadataAddress); } using (XmlReader metadataReader = XmlReader.Create(metadataAddress)) { MetadataSerializer serializer = new MetadataSerializer() { CertificateValidationMode = X509CertificateValidationMode.None }; EntityDescriptor metadata = serializer.ReadMetadata(metadataReader) as EntityDescriptor; if (metadata != null) { SecurityTokenServiceDescriptor stsd = metadata.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First(); if (stsd != null) { X509RawDataKeyIdentifierClause clause = stsd.Keys.First().KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First(); if (clause != null) { return clause.GetX509RawData(); } throw new Exception("The SecurityTokenServiceDescriptor in the metadata does not contain the Signing Certificate in the <X509Certificate> element"); } throw new Exception("The Federation Metadata document does not contain a SecurityTokenServiceDescriptor"); } throw new Exception("Invalid Federation Metadata document"); } } }
Si una vez realizado estos cambios, volvemos a ejecutar la aplicación y realizamos una llamada desde el navegador, veremos cómo todas las llamadas pasan por nuestro validador personalizado, el cuál rechazará todas las llamadas que no contenga un token de seguridad validado.
El último paso del ejemplo será realizar un cliente C# que sea capaz de llamar al servicio WebAPI pasando un token de seguridad válido para nuestro STS.
En este caso he creado un proyecto de Test, al que he añadid el paquete “Windows Azure Authentication Library”, el cuál simplifica enormemente el trabajo con WIF, ya sea con ACS o con Windows Azure Active Directory.
El siguiente código muestra cómo es posible realizar una llamada al servicio WebAPI.
En el ejemplo se hace uso de la clase AuthenticationContext disponible en WAAL, en la cuál indicamos el namespace de ACS con el que estemos trabajando, así como el nombre de la relaying party para el cuál queremos obtener un token de seguridad.
El método AcquireToken nos mostraré una interfaz de usuario en función del proveedor o proveedores configurados en ACS, para que podamos autenticarnos.
Una vez autenticados, podremos generar un token para poder mandarlo en las cabeceras de autenticación. En este caso, el delegationHandler que hemos desarrollado anteriormente validará correctamente el token y establecerá la identidad en la llamada.
[TestClass] public class DemoWebAPITests { [TestMethod] [TestCategory("Integration")] public async Task TestWebAPIService() { var authContext = new AuthenticationContext("https://estoyenlanube.accesscontrol.windows.net"); AssertionCredential credential = authContext.AcquireToken("http://localhost:29350/"); var token = credential.CreateAuthorizationHeader(); HttpClient httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", token); var response = await httpClient.GetStringAsync("http://localhost:29350/api/values"); } }