paint-brush
¿Por qué necesita JWT en su proyecto ASP.NET Core?por@igorlopushko
7,703 lecturas
7,703 lecturas

¿Por qué necesita JWT en su proyecto ASP.NET Core?

por Igor Lopushko16m2024/02/13
Read on Terminal Reader

Demasiado Largo; Para Leer

La historia trata sobre cómo crear una API web para generar JWT y luego usarlo para autorización en la API web CRUD.
featured image - ¿Por qué necesita JWT en su proyecto ASP.NET Core?
Igor Lopushko HackerNoon profile picture
0-item

JWT significa JSON Web Token y es un mecanismo de autorización, no de autenticación. Entonces, averigüemos cuál es la diferencia entre esos dos.


La autenticación es el mecanismo que permite verificar que el usuario es exactamente quien dice ser. Es un proceso de inicio de sesión en el que un usuario proporciona un nombre de usuario y contraseña, y el sistema los verifica. Entonces la autenticación responde a la pregunta: ¿quién es el usuario?


La autorización es el mecanismo que permite verificar qué derechos de acceso tiene el usuario a un determinado recurso. Es un proceso de otorgar a los usuarios algunos roles y un conjunto de permisos que tiene un rol en particular. Entonces, la autorización responde a esa pregunta: ¿qué derechos tiene el usuario en el sistema?

Autenticación versus autorización


Es importante comprender que la autenticación siempre es lo primero y la autorización lo segundo. En otras palabras, no puede obtener permiso antes de verificar su identidad. ¿Pero cuáles son los métodos de autorización más populares? Hay dos enfoques principales para manejar la autorización de la aplicación web.

Sesiones

Un enfoque tradicional en la web para autorizar a los usuarios es una sesión del lado del servidor basada en cookies. El proceso comienza cuando un usuario inicia sesión y un servidor lo autentica. Después de eso, el servidor crea una sesión con un ID de sesión y la almacena en algún lugar de la memoria del servidor. El servidor devuelve el ID de sesión al cliente y el cliente almacena el ID de sesión en cookies. Para cada solicitud, el cliente envía un ID de sesión como parte de la solicitud y el servidor verifica el ID de sesión en su memoria y los permisos del usuario relacionados con esta sesión.

Autorización basada en sesión

Fichas

Otro enfoque popular es utilizar tokens de autorización. El proceso comienza de manera similar cuando un usuario ingresa el inicio de sesión y las contraseñas y un cliente envía una solicitud de inicio de sesión a un servidor. En lugar de crear una sesión, el servidor genera un token firmado con el token secreto. Luego, el servidor devuelve el token al cliente y el cliente debe almacenarlo en un almacenamiento local. De manera similar al enfoque basado en sesiones, el cliente debe enviar un token al servidor para cada solicitud. Sin embargo, el servidor no almacena ninguna información adicional sobre la sesión del usuario. El servidor debe validar que el token no haya cambiado desde que fue creado y firmado con la clave secreta.

Autorización basada en token

Sesión frente a token

El enfoque de autorización basado en sesiones puede ser vulnerable a un ataque conocido como falsificación de solicitudes entre sitios (CSRF). Es un tipo de ataque cuando el atacante apunta a un sitio en el que ha iniciado sesión para realizar acciones que no tenía intención de realizar, como realizar un pago o cambiar una contraseña.


Otra cosa es que cuando se utiliza un enfoque de autorización basado en sesiones se crea una sesión con estado entre un cliente y un servidor. El problema es que si un cliente quiere acceder a diferentes servidores en el ámbito de la misma aplicación, esos servidores tienen que compartir un estado de sesión. En otro caso, será necesario autorizar al cliente en cada servidor ya que la sesión va a ser diferente.

Intercambio de estado de autorización basado en sesión


Por otro lado, el enfoque de autorización basado en tokens no requiere almacenar datos de sesión en el lado del servidor y puede simplificar la autorización entre múltiples servidores.


Sin embargo, un atacante aún puede robar los tokens y también puede resultar difícil invalidarlos. Veremos los detalles y cómo manejar la invalidación más adelante en este artículo.

JWT

JSON Web Token (JWT) es un estándar abierto que define una forma compacta y autónoma de transmitir información de forma segura entre partes como un objeto JSON. Esta información se puede verificar y confiar porque está firmada digitalmente. Los JWT se pueden firmar usando un secreto (con el algoritmo HMAC ) o un par de claves pública/privada usando RSA o ECDSA .

estructura JWT

Los JSON Web Tokens constan de tres partes separadas por puntos .


  • Encabezamiento
 { "alg": "HS256", "typ": "JWT" }

El encabezado normalmente consta de dos partes: el tipo de token y el algoritmo de firma que se utiliza.


  • Carga útil
 { "sub": "1234567890", "name": "John Doe", "admin": true }

La carga útil contiene los reclamos, que son declaraciones sobre el usuario. Luego, la carga útil se codifica en Base64Url para formar la segunda parte del token web JSON. Puede encontrar una descripción de los campos estándar que se utilizan como reclamos aquí .


  • Firma
 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

Para crear la parte de la firma, debe tomar el encabezado codificado, la carga útil codificada, un secreto y el algoritmo especificado en el encabezado y firmarlo.


El token suele tener el siguiente aspecto:

xxxxx.yyyyy.zzzzz


Puede navegar a jwt.io y depurar un token de muestra o el suyo propio. Simplemente pegue su token en el campo Codificado y seleccione el Algoritmo de firma del token.

depurador jwt.io

proyecto .NET

Ahora que tenemos conocimientos teóricos sobre cómo funciona JWT, podemos aplicarlos al proyecto de la vida real. Supongamos que tenemos una API simple que representa operaciones CRUD para la entidad cafetera. Vamos a crear un proyecto ASP.NET Core API que represente Coffee API. Después de eso, crearemos otro proyecto API de ASP.NET Core que representaría una API de identidad que podría generar JWT. En la vida real, probablemente usaría Identity Server , Okta o Auth0 para fines de autenticación/autorización. Sin embargo, crearíamos nuestra propia API de identidad para demostrar cómo generar JWT. Cuando finaliza la API de identidad, podemos llamar a su controlador y generar JWT en función de los datos del usuario. Además, podemos proteger la API de Coffee con una configuración de autorización que requiere pasar JWT con cada solicitud.

Panorama del proyecto .NET

API de café

Primero, vamos a crear un proyecto API ASP.NET Core simple que represente Coffee API. Aquí está la estructura de este proyecto:

API de café: estructura del proyecto


Comencemos con Coffee.cs en la carpeta Model . Es una entidad simple con propiedades de Id y Name .

 namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } }


Necesitamos almacenar nuestras entidades mientras trabajamos con la API. Entonces, introduzcamos un almacenamiento en memoria simple. Se encuentra en el archivo Storage.cs en la carpeta Data .

 namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } }


Necesitamos una clase que represente solicitudes a la API de Coffee. Entonces, creemos CoffeeRequest.cs en la carpeta Contracts .

 namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } }


Cuando termine, podemos implementar CoffeeController.cs en la carpeta Controller que representa las operaciones CRUD para la entidad de café.

 using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } }


La API de Coffee está lista y podemos ejecutar el proyecto y ver la interfaz de usuario de Swagger de la siguiente manera:

API de café: interfaz de usuario Swagger

API de identidad

Creemos otro proyecto de API de ASP.NET Core que represente la API de identidad. Aquí está la estructura de este proyecto:

API de identidad: estructura del proyecto

Comencemos con TokenGenerationRequest.cs en la carpeta Contracts , que representa la solicitud para la generación de un nuevo JWT con propiedades Email y Password .

 namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } }


Necesitamos implementar solo TokenController.cs que representa la lógica de generación JWT. Pero antes de hacerlo, es necesario instalar el paquete Microsoft.AspNetCore.Authentication.JwtBearer NuGet.

 using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } }


Tenga en cuenta que las constantes sensibles como SecretKey , Issuer y Audience deben colocarse en algún lugar de la configuración. Están codificados solo para simplificar este proyecto de prueba. El campo Lifetime está configurado en 20 minutos, lo que significa que el token será válido durante ese tiempo. También puede configurar este parámetro.


Ahora podemos ejecutar el proyecto y ver la interfaz de usuario de Swagger de la siguiente manera:

API de identidad: interfaz de usuario Swagger


Hagamos una llamada al punto final /token y generemos un nuevo JWT. Pruebe la siguiente carga útil:

 { "email": "[email protected]", "password": "password" }


La API de identidad generará el JWT correspondiente:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM

Habilitación de la autorización en la API de Coffee

Ahora, cuando la API de identidad esté lista y nos proporcione tokens, podremos proteger la API de Coffee con autorización. Nuevamente es necesario instalar el paquete Microsoft.AspNetCore.Authentication.JwtBearer NuGet.


Necesitamos registrar los servicios requeridos mediante servicios de autenticación. Agregue el siguiente código al archivo Program.cs justo después de crear un generador.

 var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization();


Es importante recordar que el orden en el middleware es importante. Habilitamos la autenticación llamando al método AddAuthentication() y especificando JwtBearerDefaults.AuthenticationScheme como esquema de autenticación. Es una constante que contiene un valor Bearer .

 namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } }


Necesitamos especificar TokenValidationParameters que describe qué parámetros de JWT se validarán durante la autorización. También especificamos IssuerSigningKey similar a signingCredentials en Identity API para verificar la firma JWT. Consulte más detalles sobre TokenValidationParameters aquí .


El siguiente fragmento de código agrega middleware al constructor que habilita capacidades de autenticación y autorización. Debe agregarse entre los métodos UseHttpsRedirection() y MapControllers() .

 app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();


Ahora podemos usar el atributo Authorize sobre el controlador o sus acciones. Al aplicar este código, ahora todas las acciones en CoffeeController están protegidas con un mecanismo de autorización y JWT debe enviarse como parte de la solicitud.

 [Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { ..


Si realizamos una llamada a cualquier punto final de la API de Coffee, podemos depurar HttpContext.User y ver que está completo y tiene una Identity con las reclamaciones que hemos especificado en JWT. Es algo importante para comprender cómo ASP.NET Core maneja la autorización internamente.

API de café: las reclamaciones se completan a partir del JWT

Agregar autorización a la interfaz de usuario de Swagger

Hicimos un gran trabajo para proteger Coffee API con la autorización. Pero si ejecuta el proyecto Coffee API y abre Swagger UI, no podrá enviar JWT como parte de la solicitud. Para solucionarlo, necesitamos actualizar el archivo Program.cs con el siguiente código:

 builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); });


Después de eso, podremos ver el botón Autorizar en la esquina superior derecha:

API de café: apareció el botón Autorizar


Al hacer clic en el botón Autorizar podrá ingresar JWT de la siguiente manera:

API de café: ingrese el valor JWT

Utilice Postman para realizar pruebas

No puede limitarse a utilizar Swagger UI y puede realizar pruebas de la API a través de la herramienta Postman. Primero llamemos al punto final /token de la API de identidad. Necesitamos especificar el encabezado Content-Type con el valor application/json en la sección Encabezados ya que usaremos JSON como carga útil.

API de identidad: especificar encabezados


Después de eso, podemos llamar al punto final /token y obtener un nuevo JWT.

API de identidad: generar JWT


Ahora podemos copiar JWT y usarlo para llamar a Coffee API. Necesitamos especificar un encabezado Content-Type similar a la API de identidad si queremos probar, crear y actualizar puntos finales. El encabezado Authorization también debe configurarse con el valor Bearer [your JWT value] . Después de eso, simplemente presione el botón Enviar y vea el resultado.

API de café: obtenga todas las entidades

Autorización basada en roles

Como recordará, la parte de carga útil de JWT es un conjunto de reclamos con valores que son exactamente pares clave-valor. La autorización basada en roles permite diferenciar el acceso a los recursos de la aplicación según el rol al que pertenece el usuario.


Si actualizamos el método Create() en el archivo TokenController.cs en Identity API con el código que agrega un nuevo reclamo para el rol; Podemos manejar la autenticación basada en roles en la API de Coffee. ClaimTypes.Role es un nombre predefinido de la reclamación de rol.

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") };


Actualice el atributo Authorize en el archivo CoffeeController.cs especificando el nombre del rol:

 [Authorize(Roles = "Barista")]


Ahora, todos los usuarios que realicen una llamada a Coffee API deben tener el rol de reclamo con el valor Barista . De lo contrario, obtendrán el código de estado 403 Forbidden .

Autorización basada en reclamos

Un atributo Authorize puede manejar fácilmente la autenticación basada en roles. Pero ¿qué pasa si no es suficiente y queremos diferenciar el acceso en función de algunas propiedades del usuario como la edad o cualquier otra? Probablemente ya haya adivinado que puede agregar sus reclamos a JWT y usarlos para crear una lógica de autorización. La autorización basada en roles en sí misma es un caso especial de autorización basada en notificaciones, del mismo modo que una función es el mismo objeto de notificación de un tipo predefinido.


Actualicemos el método Create() en el archivo TokenController.cs en Identity API con el código que agrega un nuevo reclamo IsGourmet .

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") };


En el archivo Program.cs en Coffee API, necesitamos crear una política que verifique un reclamo y pueda usarse en el atributo Authorize . El siguiente código debe agregarse justo después de la llamada al método AddAuthentication() .

 builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); });


Actualice el atributo Authorize en el archivo CoffeeController.cs especificando el nombre de la política:

 [Authorize(Policy = "OnlyForGourmet")]

Resumen

¡Felicidades! Hiciste un gran esfuerzo al aprender JWT en .NET. Ahora, debe tener un conocimiento sólido de los principios de JWT y de por qué es importante utilizarlo para realizar la autorización en aplicaciones .NET. Pero apenas hemos arañado la superficie en el área de autenticación y autorización en aplicaciones ASP.NET Core.


Sugiero consultar la documentación de Microsoft sobre los temas que analizamos en este artículo. También hay muchas capacidades integradas para autorización y gestión de roles en la plataforma .NET. Una buena adición a este artículo podría ser la documentación de Microsoft sobre autorización.