Securiza tu WebApi con tokens autogenerados

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 <token>”:

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!

15 comentarios sobre “Securiza tu WebApi con tokens autogenerados”

  1. Muy buen post, como siempre.

    Una duda, dices que el token contiene un ticket de autenticación cifrado, ¿con qué clave lo cifra? ¿Es configurable?

    Lo digo por si tienes escenarios en que haya varios servidores, por ejemplo para balancear carga, o tienes dos aplicaciones que quieres que compartan el mismo token.

  2. Muy bueno!
    Yo estoy probando la autenticación externa en Web Api con google. Me estoy basando en la plantilla por defecto de Web Api (2.2 y VS 2013) pero el método
    var info = await Authentication.GetExternalLoginInfoAsync();
    en la acción RegisterExternal del controlador Account siempre me devuelve null.
    He probado a realizar esto mismo en un proyecto ASP.NET MVC 5 y sin problemas, funciona correctamente.

    Te describo los pasos que he realizado:
    1º. Registro la aplicación en la consola de google (documento seguido para esto: http://www.asp.net/mvc/overview/security/create-an-aspnet-mvc-5-app-with-facebook-and-google-oauth2-and-openid-sign-on)
    2º. En el fichero Startup.Auth.cs, activo la autenticación mediante google con los datos obtenidos en el 1º:
    app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
    {
    ClientId = «XXXXX.apps.googleusercontent.com»,
    ClientSecret = «XXXXX»
    });
    2º. Test mediante fiddler:
    a) Obtengo proveedores:
    GET http://localhost:4592/api/Account/ExternalLogins?returnUrl=%2F&generateState=true
    Respuesta:
    [{«Name»:»Google»,»Url»:»/api/Account/ExternalLogin?provider=Google&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A4592%2F&state=OZIxoz79ycpK0fIAemUUc0T5OhFS2YdtLVGX8SEtl6A1″,»State»:»OZIxoz79ycpK0fIAemUUc0T5OhFS2YdtLVGX8SEtl6A1″}]
    b) Obtener el token de acceso de google para poder registrar al nuevo usuario:
    GET http://localhost:4592/api/Account/ExternalLogin?provider=Google&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A4592%2F&state=OZIxoz79ycpK0fIAemUUc0T5OhFS2YdtLVGX8SEtl6A1
    Respuesta:
    http://localhost:4592/#access_token=myAccessToken&token_type=bearer&expires_in=1209600&state=OZIxoz79ycpK0fIAemUUc0T5OhFS2YdtLVGX8SEtl6A1
    c) Mediante el access token obtenido intento registrar el nuevo usario:
    POST http://localhost:4592/api/Account/RegisterExternal
    Content-Type: application/json
    Authorization: Bearer myAccessToken
    {
    «Email»: «myemail@gmail.com»
    }
    Es aquí donde recibo un error 500: Internal server error, que viendo el código me indica que se ha producido porque el resultado de Authentication.GetExternalLoginInfoAsync() es null.
    Mi idea es usar Web Api como backend para el desarrollo de una aplicación móvil, pero primero estoy probando cómo funciona todo esto…
    Probablemente esté haciendo algo mal, ¿no sé si te suena este problema?
    A ver si me puedes ayudar
    ¡¡Muchas gracias!!

  3. Buenas Eduard,

    ¿Si quisieras enviar algún campo más (por ejemplo lo que se introduce para validar un captcha) como lo harías? ¿Cómo extenderías el OAuthGrantResourceOwnerCredentialsContext?

    Saludos.

  4. Buenas Eduard,

    ¿Si quisieras enviar algún campo más (por ejemplo lo que se introduce para validar un captcha) como lo harías? ¿Cómo extenderías el OAuthGrantResourceOwnerCredentialsContext?

    Saludos.

  5. Hola, este ha sido un excelente tutorial, por fin algo claro con lo que me puedo guiar, efectivamente logre crear el token desde mi web api .net.

    Pero me nace una duda, leyendo sobre tokens dice que uno puede meter mas datos como el usuario y otros datos utiles, y estos se ven separados por un punto «.». como se podria completar para agregar por ejemplo el usuario en code64 dentro del token?

  6. Hola buen dia estoy tratanto de implementar los bearer token como medio de autenticacion , pero me surge una duda , ¿Cómo puedo invalidar un token en caso de que este se vea comprometido?

    1. Buenas!
      Con la implementación de este post NO puedes. Hay muchos más temas a hablar sobre los tokens, y en efecto la invalidación es uno de ellos.
      Antes de nada debemos tener presente que «bearer token» significa «da acceso a quien sea que tenga este token», son como un cheque al portador. Hay otras implementaciones de tokens (p. ej. tokens MAC) que son firmados a través de una clave secreta compartida por cliente (una app) y servidor. En este caso, se puede invalidar cualquier token MAC simplemente invalidando el cliente (app) en el servidor. Sería el equivalente a un cheque firmado, donde antes de cobrarlo se puede verificar que quien lo tiene es quien dice ser (mediante la firma). Los tokens MAC son los que se usaban en OAuth1 y son los culpables del 99% de errores en la implementación de flujos OAuth1. De ahí que en OAuth2 (y OIDC que está basado en OAuth2) se usen preferentemente bearer tokens.

      Entonces, si asumimos bearer tokens, como podemos invalidarlos? La respuesta sencilla es «no podemos», pero habitualmente lo que se hace es que «caduquen». Realmente un token contiene información y entre esa información se encuentra una fecha de caducidad. Para evitar que el token pueda ser falseado por el cliente se firma mediante un sistema de clave pública-privada: la clave privada es conocida solo por el emisor de tokens y la clave pública debe ser conocida por cualquier recurso (p. ej. API) que deba validar tokens. De este modo podemos descartar cualquier token caducado. Por supuesto, si caducamos tokens debemos tener un sistema para que usuarios legítimos puedan re-obtener tokens sin necesidad de re-entrar credenciales. Este mecanismo son los «tokens de refresco». A grandes rasgos (y MUY SIMPLIFICADO) la idea queda así:

      1. Acceso a recurso (API) sin token. Se genera un 401 (no autorizado)
      2. El usuario entra login/password. El cliente recibe DOS tokens (bearer y refresco)
      3. Se accede a recursos usando el bearer token…
      4. Al cabo de un rato se recibe un 401 a pesar de enviar el token.
      5. Cliente asume que el token ha caducado. Usa el token de refresco para obtener un par nuevo (bearer, refresco)
      6. Usa el nuevo bearer para acceder a la API

      Efectivamente eso complica el cliente que debe encargarse de toda esa logística de forma transparente para el usuario. La clave aquí es que el token de refresco se lo guarda el cliente y solo circula en la red para una única llamada: la de obtener token nuevo, lo que además puede invalidar el propio token de refresco. De este modo si un bearer token es comprometido solo será válido durante X tiempo. Es habitual tener bearer tokens con duración de una hora (o menos) lo que limita mucho los posibles ataques.

      Habría más maneras de invalidar tokens (siempre basándose en la información que el token contiene), pero lo habitual es hacer eso, que caduquen.

      Veo que eso probablemente merezca un post propio…

  7. Hola al intentar acceder a http://localhost:30088/token para validar el usuario y obtener el token, sale este error:

    XMLHttpRequest no puede cargar http: // localhost: 30088 / token. Ningún encabezado ‘Access-Control-Allow-Origin’ está presente en el recurso solicitado. Origen ‘http: // localhost: 4200’

    Pero mi app angular 2 (cliente) y webapi están en el mismo PC y puse para habilitar cors, pero deberia funcionar ya que estan en el mismo dominio.

    ¿Cual podría ser el problema?

    1. Hola Carlos!
      CORS se basa en el concepto de orígen, no de dominio. Un orígen es dominio+puerto.
      Así pues localhost:30088 es un origen y localhost:4200 es otro, por lo que debes tener CORS.

      El mensaje que te da es precisamente el de CORS… Poca cosa más puedo decirte!

    2. hola, tengo un problema parecido con mi aplicacion en angular 4: obtengo mi token y al hacer la consulta al wep api me retorna ese mensaje (Ningún encabezado ‘Access-Control-Allow-Origin’ está presente en el recurso solicitado. Origen ‘http: // localhost: 4200’) si lo pruebo con postman si funciona correctamente. ¿Alguien sabe que puede ser?

  8. Buenas tardes
    He seguido este post pero me da un error:

    Error 1 ‘Owin.IAppBuilder’ no contiene una definición de ‘UseWebApi’ ni se encontró ningún método de extensión ‘UseWebApi’ que acepte un primer argumento de tipo ‘Owin.IAppBuilder’ (¿falta una directiva using o una referencia de ensamblado?) D:\Macia\OtrosProgramas\WebApiConJWTEspanol\WebApiConJWTEspanol\Startup.cs 22 17 WebApiConJWTEspanol

    ¿ Me puedes ayudar ? Trabajo con visual studio 2013 ?
    Gracias

Responder a crisdanr Cancelar respuesta

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