Securiza tu WebApi con tokens autogenerados

 ·  ☕ 6 min  ·  ✍️ eiximenis

    Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí

    El escenario que vamos a abordar en este post es el siguiente: tienes una API creada con ASP.NET WebApi y quieres que sea accesible a través de un token. Pero en este caso quieres ser tu quien proporcione el token y no un tercero como facebook, twitter o Azure Mobile Services (como p. ej. en el escenario que cubrimos en este post). Para ello nuestra API expondrá un endpoint en el cual se le pasarán unas credenciales de usuario (login y password) para obtener a cambio un token. A partir de ese momento todo el resto de llamadas de la API se realizarán usando este token y las credenciales del usuario no seran necesarias más.

    Para empezar crea un proyecto ASP.NET con el template “Empty” pero asegúrate de marcar la checkbox de “Web API” para que nos la incluya por defecto. Luego agregamos los paquetes para hospedar OWIN, ya que vamos a usar componentes OWIN tanto para la creación de los tokens oAuth como su posterior validación. Así pues debes incluir los paquetes “Microsoft.AspNet.WebApi.Owin” y “Microsoft.Owin.Host.SystemWeb”.

    El siguiente paso será crear una clase de inicialización de Owin (la famosa Startup). Para ello puedes hacer click con el botón derecho sobre el proyecto en el solution explorer y seleccionar “Add –> OWIN Startup class” o bien crear una clase normal y corriente llamada Startup. El código inicial es el siguiente:

    1. [assembly: OwinStartup(typeof(OauthProviderTest.Startup))]
    2.  
    3. namespace OauthProviderTest
    4. {
    5.     public class Startup
    6.     {
    7.         public void Configuration(IAppBuilder app)
    8.         {
    9.             var config = new HttpConfiguration();
    10.             WebApiConfig.Register(config);
    11.             ConfigureOAuth(app);
    12.             app.UseWebApi(config);
    13.         }
    14.     }
    15. }

    La clase WebApiConfig es la que configura WebApi y la generó VS al crear el proyecto (está en la carpeta App_Start). Nos falta ver el método ConfigureOAuth que veremos ahora mismo. Observa que el método ConfigureOAuth se llama antes del app.UseWebApi, ya que vamos a añadir middleware OWIN en el pipeline http y debemos hacerlo antes de que se ejecute WebApi. Y por cierto, dado que ahora inicializamos nuestra aplicación usando OWIN puedes eliminar el fichero Global.asax, ya que no lo necesitamos para nada.

    Veamos ahora el método ConfigureOAuth. En este método debemos añadir el middleware OWIN para la creación de tokens OAuth. Para ello podemos usar el siguiente código:

    1. public void ConfigureOAuth(IAppBuilder app)
    2. {
    3.     var oAuthServerOptions = new OAuthAuthorizationServerOptions()
    4.     {
    5.         AllowInsecureHttp = true,
    6.         TokenEndpointPath = new PathString("/token"),
    7.         AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
    8.         Provider = new CredentialsAuthorizationServerProvider(),
    9.     };
    10.     app.UseOAuthAuthorizationServer(oAuthServerOptions);
    11. }

    Con ello habilitamos un endpoint (/token) para generar los tokens oAuth. El proveedor de dichos tokens es la clase CredentialsAuthorizationServerProvider (que veremos a continuación). Esta clase será la que recibirá las credenciales (login y password), las validará y generará un token oAuth.

    Por supuesto nos falta ver el código para validar las credenciales y aquí es donde entra la clase CredentialsAuthorizationServerProvider. Esa clase es la que recibe el login y el password del usuario, los valida y crea el token oAuth:

    1. public class CredentialsAuthorizationServerProvider : OAuthAuthorizationServerProvider
    2. {
    3.     public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    4.     {
    5.         context.Validated();
    6.     }
    7.  
    8.     public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    9.     {
    10.  
    11.         context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
    12.  
    13.         using (TestContext db = new TestContext())
    14.         {
    15.             var user = db.Users.FirstOrDefault(u => u.Login == context.UserName && u.Password == context.Password);
    16.             if (user == null)
    17.             {
    18.                 context.SetError("invalid_grant", "The user name or password is incorrect.");
    19.                 return;
    20.             }
    21.         }
    22.  
    23.         var identity = new ClaimsIdentity(context.Options.AuthenticationType);
    24.         identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
    25.         identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
    26.         context.Validated(identity);
    27.     }
    28. }

    Lo único remarcable es la primera línea del método GrantResourceOwnerCredentials que se encarga de habilitar CORS. WebApi tiene soporte para CORS, pero el endpoint /token no está gestionado por WebApi si no por el middleware OWIN así que debemos asegurarnos de que mandamos las cabeceras para habilitar CORS. El resto del código es un acceso a la BBDD usando un contexto de EF para encontrar el usuario con el login y el password correcto. Por supuesto en un entorno de producción eso no se haría así. Este código no tiene en cuenta que las passwords deben guardarse como un hash en la BBDD. Si quieres acceder a BBDD directamente debes generar el hash de los passwords aunque si una mejor opción es usar Identity (y la clase UserManager) para acceder a los datos de los usuarios. Una vez validado que las credenciales son correctas creamos la ClaimsIdentity y generamos el token correspondiente.

    Para probarlo podéis hacer un POST a /token con las siguientes características:

    • Content-type: application/x-www-form-urlencoded
    • En el cuerpo de la petición añadir los campos:
    • grant_type = “password”
    • username = “Login del usuario”
    • password = “Password del usuario”

    Es decir como si tuvieses un formulario con esos campos y lo enviaras via POST al servidor. Os pongo una captura de la petición usando postman:

    image

    Se puede ver que la respuesta es el token, el tiempo de expiración (que se corresponde con el valor de la propiedad AccessTokenExpireTimeSpan) y el tipo de autenticación que es bearer (ese es el valor que deberemos colocar en la cabecera Authorization).

    A partir de ese punto tenemos un token válido y nos olvidamos de las credenciales del usuario. Al menos hasta que el token no caduque. Cuando el token caduque, se deberá generar uno nuevo con otro POST al endpoint /token.

    El siguiente punto es habilitar WebApi para que tenga en cuenta esos tokens. Hasta ahora nos hemos limitado a generar tokens, pero WebApi no hace nada con ellos. De hecho no habilitamos WebApi si no que añadimos otro módulo OWIN para autenticarnos en base a esos tokens. El proceso ocurre antes y es transparente a WebApi. Para ello debemos añadir las siguientes líneas a la clase Startup al final del método ConfigureOAuth:

    1. var authOptions = new OAuthBearerAuthenticationOptions()
    2. {
    3.     AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active
    4. };
    5. app.UseOAuthBearerAuthentication(authOptions);

    Así añadimos el módulo de OWIN que autentica en base a esos tokens. Para hacer la prueba vamos a crear un controlador de WebApi y vamos a indicar que es solo para usuarios autenticados:

    1. [Authorize]
    2. public class SecureController : ApiController
    3. {
    4.     public IHttpActionResult Get()
    5.     {
    6.         return Ok("Welcome " + User.Identity.Name);
    7.     }
    8. }

    Y ahora para probarlo hacemos un GET a la URL del controlador (/api/secure) y tenemos que pasar la cabecera Authorization. El valor de dicha cabecera es “Bearer ”:

    image

    Y con esto deberíamos obtener la respuesta del controlador. En caso de que no pasar la cabecera o que el token fuese incorrecto el resultado sería un HTTP 401 (no autorizado).

    Unas palabras sobre los tokens

    Fíjate que en ningún momento guardamos en BBDD los tokens de los usuarios y esos tokens son válidos durante todo su tiempo de vida incluso en caso de que el servidor se reinicie. Eso es así porque el token realmente es un ticket de autenticación encriptado. El middleware de OWIN cuando recibe un token se limita a desencriptarlo y en caso de que sea válido, extrae los datos (los claims de la ClaimIdentity creada al generar el token) y coloca dicha ClaimIdentity como identidad de la petición. Es por eso que en el controlador podemos usar User.Identity.Name y recibimos el nombre del usuario que entramos.

    Por lo tanto cualquier persona que intercepte el token podrá ejecutar llamadas “en nombre de” el usuario mientras este token sea válido. A todos los efectos poseer el token de autenticación equivale a poseer las credenciales del usuario, al menos mientras el token sea válido. Tenlo presente si optas por ese mecanismo de autenticación: si alguien roba el token, la seguridad se ve comprometida mientras el token sea válido. Por supuesto eso no es distinto al caso de usar una cookie, donde el hecho de robar la cookie compromete la seguridad de igual forma. Y los tokens son más manejables que las cookies y dan menos problemas, en especial en llamadas entre orígenes web distintos.

    ¡Espero que este post os resulte interesante!

    Si quieres, puedes invitarme a un café xD

    eiximenis
    ESCRITO POR
    eiximenis
    Compulsive Developer