Autor en Google+
Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

12 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 19 de diciembre de 2017
ASP.NET Core MVC
JSON Web Tokens, o JWT para los amigos, es sin duda una de las fórmulas más utilizadas para autenticación en servicios o APIs HTTP gracias a su sencillez de uso y a la seguridad que aportan en determinados escenarios frente a otras opciones como las populares cookies.

En este post vamos a ver cómo implementar funcionalidades básicas de generación de tokens JWT en ASP.NET Core MVC, y cómo asegurar nuestros APIs utilizándolos para autenticar a los usuarios.

Lo básico sobre JWT

En la práctica, un JWT consiste en una simple cadena de caracteres que acompañará a cada petición que realizamos sobre un servidor para que éste pueda conocer nuestra identidad de forma segura. Podéis ver un ejemplo a continuación:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImpvaG5kb2UifQ.OZuaG00NsY9ALMgtF-K7YN4XVYtF4mbOHMgh9SZukc4
Esta cadena está formada por tres secciones, que en el ejemplo anterior podemos ver que aparecen separadas por un punto:
  • Header, que incluye metainformación sobre el token, por ejemplo su tipo o el algoritmo de hashing utilizado.
     
  • Payload, la información que queremos incluir en el token, normalmente claims o atributos del usuario.
     
  • Signature, la firma de las dos secciones anteriores que asegurará que el token JWT es válido.
Técnicamente estos tokens podrían viajar en cualquier parte de la petición (query string, headers, cuerpo…), pero lo más normal es que lo hagan en el encabezado Authentication, como se muestra en el siguiente ejemplo:
GET http://localhost:57284/api/values HTTP/1.1
Host: localhost:57284
Content-Length: 0
Content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImpvaG5kb2UifQ.OZuaG00NsY9ALMgtF-K7YN4XVYtF4mbOHMgh9SZukc4
Para que el cliente de un API pueda incluir el token en la petición, éste debe haber sido generado previamente, ya sea desde el propio servidor que aloja los servicios o bien desde terceros de confianza. El cualquier caso, el workflow completo sería el siguiente:
  1. El cliente realiza una primera petición al servidor que generará el token suministrándole (por ejemplo) el nombre y contraseña del usuario actual.
     
  2. Si las credenciales son correctas, como respuesta el servidor responderá con el token.
     
  3. El cliente almacena el token para su uso posterior.
     
  4. En todas las llamadas al API, el cliente incluiría dicho token en el encabezado Authorization.
     
  5. Al recibir las peticiones, el servidor decodificará el token, comprobará su validez, y establecerá el usuario actual para el hilo de la petición, de forma que podremos acceder a él de la forma habitual (por ejemplo, a través de User.Identity).

Generación de tokens JWT

En ASP.NET Core, la generación de tokens es casi trivial. Lo habitual será que la primera llamada del cliente la efectúe contra una acción que reciba las credenciales del usuario y retorne el token generado.

Una implementación simplificada podría ser la siguiente:
public class TokenController : Controller
{
    [HttpPost("/token")]
    public IActionResult Get(string username, string password)
    {
        if (username == password)
            return Ok(GenerateToken(username));
        else
            return Unauthorized();
    }

    private string GenerateToken(string username)
    {
        var header = new JwtHeader(
            new SigningCredentials(
                new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes("your-secret-key-here")
                ),
                SecurityAlgorithms.HmacSha256)
        );

        var claims = new Claim[]
        {
            new Claim(JwtRegisteredClaimNames.UniqueName, username),
        };        
        var payload = new JwtPayload(claims);

        var token = new JwtSecurityToken(header, payload);
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
Como podemos observar, el algoritmo de verificación de credenciales no es muy seguro porque sólo comprueba que username y password coincidan (¡niños, no hagáis esto en casa! ;)), pero seguro que pilláis la idea y sabríais sustituirlo por otra cosa más razonable. La cuestión es que cuando decidimos que la comprobación es exitosa, generamos el token invocando a GenerateToken() y lo retornamos.

El código de `GenerateToken()’ también se entiende bastante bien. En primer lugar componemos la sección de encabezado del token, indicando el algoritmo utilizado (SHA-256) y credenciales de la firma digital. Observad que el texto “your-secret-hey-here” es la clave simétrica que más adelante se utilizará para comprobar la validez de la firma. Es decir, debe conocerse tanto en el lugar en que se genera el token como donde se recibe, estén o no en el mismo servidor.

A continuación, componemos el payload, o la información personalizada que queremos introducir en el token. Normalmente serán datos como el nombre de usuario, un ID único, o algo que nos permita identificarlo de forma inequívoca en el lado servidor. Es obvio que cuanta más información introduzcamos en el payload, más cómodo nos resultará a la hora de recuperarla en el lado servidor, aunque a cambio incrementaremos el peso en bytes de las peticiones.

En el ejemplo anterior, simplemente añadimos un claim con el nombre único del usuario.
Muy importante: el payload de estos tokens puede ser consultado fácilmente utilizando herramientas de decodificación como esta, esta otra o muchas más, por lo que sólo deben incluirse datos no sensibles. Si tenéis un rato, os recomiendo que probéis a copiar y pegar en una de estas herramientas cualquiera de los tokens mostrados en este post para concienciaros con este tema.
Por último, generamos el token con los dos elementos anteriores, invocando al método WriteToken() de la clase JwtSecurityHandler, que lo retorna en forma de cadena de texto.

Obviamente, la implementación anterior es bastante simple. Es habitual que encontremos algunos claims adicionales y, sobre todo, algo de metainformación sobre el token, como quién lo ha generado, a quién va dirigido, la fecha y hora en la que puede comenzar a utilizarse y hasta cuándo es válido:
var claims = new Claim[]
{
    new Claim(JwtRegisteredClaimNames.UniqueName, username),
    new ClaimsIdentity(JwtRegisteredClaimNames.Email, email), 
};
var payload = new JwtPayload(
    issuer: "MyServer",
    audience: "MyWebApp",
    claims: claims,
    notBefore: DateTime.Now,
    expires: DateTime.Now.AddHours(1)
);
Un ejemplo de uso de la acción anterior sería el siguiente:
POST http://localhost:57284/token?username=johndoe&password=johndoe HTTP/1.1
Host: localhost:57284
Content-Length: 0

==============================

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Content-Length: 247

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG5kb2VAbXlzZXJ2ZXIuY29tIiwibmJmIjoxNTExNzY5OTE1LCJleHAiOjE1MTE3NzM1MTUsImlzcyI6Ik15U2VydmVyIiwiYXVkIjoiTXlXZWJBcHAifQ.oFWRNfqCesNAs3ckx1KHpcqIabx1ZVG36CO24REyYhA
Cuando el lado cliente recibe este token, debe almacenarlo en una variable en memoria o un lugar persistente como puede ser el session storage en caso de estar utilizando un navegador, de forma que pueda incluirlo en las siguientes peticiones a APIs seguras.

Autenticación con tokens JWT

La comprobación de los tokens, obtención de claims y establecimiento del Principal en el contexto de la petición es algo que debe realizarse en el punto de entrada a las API. Y de nuevo, ASP.NET Core viene al rescate y no tendremos que trabajar demasiado para conseguirlo.

Todo el trabajo sucio lo realizará el middleware de autenticación de ASP.NET Core, que deberemos posicionar estratégicamente en el pipeline para que vigile las peticiones entrantes. Esto lo veremos más abajo, de momento lo primero es configurar los servicios de autenticación en el método ConfigureServices() de la clase Startup, por ejemplo como sigue:
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
        {
            jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
            {
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes("your-secret-key-here")
                ),
                ValidIssuer = "MyServer",
                ValidAudience = "MyWebApp",
            };
        });
    ...
}
Observad que, de nuevo, este código es trivial. Tras añadir los servicios de autenticación indicando que el esquema de seguridad por defecto, añadimos la configuración específica para JWT en la que podemos destacar los siguientes elementos:
  • En IssuerSigningKey debemos introducir la clave secreta que fue utilizada para firmar digitalmente el token.
     
  • Con ValidIssuer identificamos quién consideramos que es un emisor válido para el token. Debe coincidir con lo establecido a la hora de generar el token, aunque esta comprobación puede omitirse estableciendo la propiedad ValidateIssuer a falso.
     
  • De la misma forma, ValidAudience debe contener la audiencia o destinatarios a los que se dirige el token, y coincidir con la indicada al generar el token. También puede omitirse esta comprobación poniendo a falso la propiedad ValidateAudience.
En realidad, el objeto TokenValidationParameters tiene algunas propiedades más que permiten afinar otros comportamientos de la comprobación del token, pero no tendréis problema en averiguar cuáles son y cómo se usan.
Una vez configurado, ya sólo nos queda añadir el middleware al pipeline de ASP.NET Core desde el interior del método Configure():
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    [...]
    app.UseAuthentication();
    app.UseMvc();
}
A partir de este momento, todas las peticiones entrantes que lo atraviesen serán examinadas. Si existe un encabezado “Authorization” de tipo bearer, su contenido será descodificado y verificado; si todo es correcto, el ClaimsPrincipal asociado a la petición será actualizado con los claims presentes en el token y podremos acceder a sus claims y otra información a través de HttpContext.User. En caso de que algo vaya mal, el middleware dejará pasar la petición aunque el usuario no estará autenticado (es decir, User.Identity.IsAuthenticated será falso).

Protección de acciones de APIs con JWT

Pues poco podemos decir aquí, puesto que el sistema de autenticación y autorización de ASP.NET Core es bastante consistente. Si queremos que determinadas acciones o controladores estén protegidas contra accesos no deseados, sólo debemos utilizar el conocido filtro [Authorize] sobre ellos, como se muestra a continuación:
[Authorize]
[Route("api/[controller]")]
public class TestController : Controller
{
    [HttpGet]
    public string Hello()
    {
        return $"Hello, {User.Identity.Name}!";
    }
}
Como recordaréis, el atributo [Authorize] hace que las peticiones dirigidas a los elementos afectados (en ejemplo, todas las acciones del controlador) sólo se permitan si ésta viene de un usuario autenticado. En caso contrario, se retornará un HTTP 401 (No autorizado) .

También podríamos utilizar este atributo para permitir el acceso a determinadas acciones o controladores únicamente a usuarios que pertenezcan a unos roles determinados, por ejemplo:
[Authorize(Roles="admins,superadmins")]
[HttpGet]
public string Hello()
{
    return $"Hey, superuser {User.Identity.Name}!";
}
Obviamente, para que que el sistema reconozca que un usuario pertenece a un rol, el token debe incorporar esta información en forma de claim. Esto podemos hacerlo en el momento de generar el token de la siguiente forma:
...
var claims = new Claim[]
{
    new Claim(JwtRegisteredClaimNames.UniqueName, username),
    new Claim(JwtRegisteredClaimNames.Email, username + "@myserver.com"),
    new Claim(ClaimTypes.Role, "admins"), 
    new Claim(ClaimTypes.Role, "supervisors"), 
};
...
Así, si el usuario está autenticado pero no pertenece a dichos roles, la respuesta será HTTP 403 (Prohibido).
No vamos a entrar en ello, pero el atributo [Authorize] da mucho más de sí. Podéis echar un vistazo a la documentación oficial sobre autorización en ASP.NET Core si queréis saber más sobre el tema.

Enviar tokens JWT al servidor

Pues ya lo hemos comentado a lo largo del post, y no tiene demasiado misterio. Desde el punto de vista del cliente, una vez obtenemos el token debemos guardarlo para adjuntarlo en las peticiones dirigidas a acciones securizadas con JWT.

Para ello, lo único que debemos hacer es añadir un encabezado Authorization de tipo bearer con el valor del token obtenido:
GET http://localhost:57284/api/values HTTP/1.1
Host: localhost:57284
Content-Length: 0
Content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImpvaG5kb2UifQ.OZuaG00NsY9ALMgtF-K7YN4XVYtF4mbOHMgh9SZukc4
Simplemente, desde el lado cliente debéis tener en cuenta varias cosas:
  • Tras solicitar un token al servidor, debéis almacenarlo en algún sitio seguro para utilizarlo en todas las llamadas que realicéis posteriormente al API. Por ejemplo, un cliente desktop o móvil podrían guardarlo directamente en una variable en memoria; en el mundo web suelen utilizarse mecanismos de persistencia como session storage, local storage o incluso cookies.
     
  • Si alguien os roba un token puede hacerse pasar por vosotros. Ojito a la seguridad, uso de HTTPS y esas cosas. No es nada que no ocurra con otros sistemas como las cookies, pero nunca está de más recordarlo.
     
  • Los tokens caducan. A la hora de generarlos, el servidor puede especificar un tiempo de validez, por lo que puede que un token deje de funcionaros en algún momento. Para evitar problemas, debéis vigilar la respuesta de las peticiones (en especial los retornos HTTP 401) y regenerar el token cuando sea necesario.
Aquí lo dejaremos por hoy. Recapitulando un poco, en este post hemos visto lo sencillo que resulta implementar en ASP.NET Core el flujo completo de la autenticación basada en tokens JWT: cómo generarlos, cómo enviarlos al servidor, cómo autenticar las peticiones que incluyen un token y cómo proteger nuestras acciones en función de la identidad del usuario.

Espero que os haya resultado interesante y os sea de utilidad para introducir este tipo de autenticación en vuestros servicios :)

Publicado en Variable not found.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

8 Comentarios:

Anónimo dijo...

Gran artículo aunque me surge una duda sobre regenerar el token ¿Cómo gestionar este aspecto?

Gracias y feliz año!

José M. Aguilar dijo...

Hola!

Muchas gracias, feliz año igualmente! :)

Si te refieres a refresh tokens, no he visto que haya nada en el framework para solucionarlo, por lo que habría que implementarlo de forma manual. Aún no he tenido necesidad de hacerlo por lo que no tengo experiencias en primera persona, pero sí veo que hay algunos ejemplos en la red sobre cómo conseguirlo, como este post.

Si simplemente hablas de regeneración (por ejemplo, cuando caduca el token actual), el workflow sería exactamente el mismo que el descrito en este post: el cliente debería pedir un token nuevo aunque para ello debería volver a enviar sus credenciales (por eso se suelen usar los refresh tokens).

Saludos!



Fernando Aguilar dijo...

Hola que tal, excelente Artículo... me ha sido muy útil, tengo una duda... y es el como trabajar con los Claims... tienes algún buen post de dónde pueda aprender más respecto a ello?

José M. Aguilar dijo...

Hola! Muchas gracias!

Pues no te sabría dirigir a uno concreto; la verdad es que hay mucha información en la red, pero para mi gusto a veces tienen demasiado "ruido", o no está muy actualizada. En cualquer caso, depende de lo que te interese de trabajar con claims.

Si lo que buscas es cómo autorizar peticiones dependiendo de los claims del usuario, quizás esta entrada de la documentación oficial pueda valerte: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/claims.

Saludos!




Fernando Aguilar dijo...

Y cómo puedo manualmente expirar el token?... por ejemplo, creo un token que dura 24 horas... pero quiero denegar el acceso al usuario inmediatamente, cómo podría expirar el token?

José M. Aguilar dijo...

Hola!

Una vez has emitido el token y está en el cliente, la revocación no es trivial. Existen varios enfoques, pero quizás el más sencillo es incluir en el token algún identificador que permita, en cada petición, comprobar si el mismo se encuentra en una "lista negra" de tokens inválidos.

Obviamente, esto complica algo el desarrollo, porque necesitarás hacer persistir los tokens generados, crear un caché (o similar) de tokens inválidos, implementar las comprobaciones... pero bueno, nada que no pueda solucionarse en un ratillo largo ;)

Saludos!

Cristian Gomez dijo...

Y que tal por las dudas para evitar robo de cookies, por las dudas en el token metes encriptado la ip, entonces cuando validas el token comparas la ip, si es distinta por las dudas le pedís que se reloguee de nuevo, podrías ser?

Incluso en un sistema medio critico no seria bueno que el tokken dure máximo 2 horas? Por ejemplo a veces amazon si pasas un rato medio largo sin hacer nada, digamos una hora que tenes la pagina abierta, al moverte de sección te pide loguearte de nuevo. tal vez entonces si por ahí el sistema es critico se podría poner que dure una hora o dos ?

Muy bueno el articulo es facil de entender y no hay que hacer tanto para tener un sistema de token

José M. Aguilar dijo...

Hola!

Por supuesto, todo lo que idees para mejorar la seguridad, será buena idea. Y limitar el tiempo de validez siempre es buena cosa, aunque siempre manteniendo un balance razonable entre la usabilidad y la seguridad.

Saludos!

Artículos relacionados: