Cómo securizar servicios WebAPI usando ACS y tokens JWT

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.

DemoACS23

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.

DemoACS24

DemoACS25

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.

DemoACS26

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.

DemoACS31

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.

DemoACS32

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");

        }
    }

DemoACS34

 

DemoACS36

Ibon Landa

bon Landa lleva más de 15 años dedicado al desarrollo de software. Durante este tiempo ha trabajado en diferentes empresas en las cuáles ha podido trabajar en diferentes entornos y tecnologías. Actualmente está focalizado principalmente en tareas de desarrollo, arquitectura, en las herramientas del ciclo de vida y en todo lo relacionado con la plataforma de Cloud Computing Microsoft Azure, área en el que ha sido reconocido como MVP. Participa de forma activa en la comunidad, escribiendo su blog, manteniendo un portal sobre Microsoft Azure y colaborando con Microsoft y grupos de usuarios en eventos de formación, talleres y giras de producto.

Deja un comentario

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