Autenticación en una minimal API con .NET 7 y JWT
❗ Es importante tener tu backend protegido para evitar fuga de información sensible.
En el siguiente artículo no te voy a contar cómo bastionar tu servidor para evitar ciberataques 👨💻, ese será tema de otro artículo.
Lo que te voy a contar en el siguiente artículo es cómo securizar tu minimal API programada con .NET7 y C# usando JWT 🔐
¿Qué es la autenticación? 🔐
La autenticación es el proceso de verificar la identidad de un usuario.
Es fundamental para proteger los datos y recursos sensibles, asegurando que solo las personas o sistemas autorizados puedan interactuar con la API.
La autenticación en una API se logra mediante el uso de credenciales.
Las credenciales pueden tomar diversas formas, usuario y contraseña, tokens de acceso, API Key, certificados digitales, biometría...
En este artículo vamos a usar JWT, que es estándar de autenticación basado en tokens.
¿Qué es JWT?
Como hemos dicho, JWT (JSON Web Token) es una forma de securizar aplicaciones basada en tokens de acceso.
Un token es una cadena alfanumérica que posee información sensible.
En el caso concreto de JWT, los tokens utilizados están codificados en Base64 y constan de tres partes separadas por puntos.
- Header (Encabezado): El encabezado consta de dos partes: el tipo de token, que es JWT, y el algoritmo de firma utilizado, como HMAC SHA256 o RSA.
Payload (Carga útil): El payload contiene los llamados "claims".
Los claims son declaraciones sobre una entidad (por lo general, el usuario) y datos adicionales.
Signature (Firma): Para verificar la autenticidad del token y asegurarse de que no haya sido alterado en el camino, se utiliza una firma digital.
La firma se crea tomando el encabezado codificado en Base64, el payload codificado en Base64, una clave secreta y el algoritmo especificado en el encabezado para generar la firma.
Esto permite que el receptor del token verifique su autenticidad.
Un ejemplo de token de JWT podría ser:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImZqbWFydGluZXpAaW5lcmNvLmNvbSIsIm5iZiI6MTY5NTExMTUxNSwiZXhwIjoxNjk1MTE1MTE1LCJpYXQiOjE2OTUxMTE1MTUsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0OjcwMTciLCJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo3MDE3In0.seJDi-0dzvwCcoNxcjnQkKQ2PeBLv9NYQ7oE1H0oVS0
Si navegas a la página oficial de JWT y pegas el token anterior podrás ver la información que contiene:
Si te fijas en la imagen, yo he pegado la firma correcta y nos dice que la firma está verificada.
En el caso de que no conozcas la firma no obtendrás un token correcto.
Las ventajas que tiene el securizar nuestras APIs con token son:
- Seguridad: Los tokens son seguros si se implementan correctamente. Como te he dicho se cifra la información para garantizar la integridad y la autenticidad de los datos transmitidos.
- Desacoplamiento: La autenticación basada en token permite el desacoplamiento del proceso de autenticación del servidor de recursos, lo que implica que se puede delegar la autenticación a un servidor de autorización separado.
- Escalabilidad: Los tokens son fácilmente escalables ya que no dependen del estado del servidor.
- Inicio de sesión único (Single Sign-On, SSO): Los tokens se pueden utilizar en sistemas de inicio de sesión único, donde un solo token puede permitir el acceso a múltiples recursos y aplicaciones.
¿Cómo se autentica en .NET con JWT?
Instalando librerías necesarias
Vamos a utilizar dos librerías oficiales de Microsoft para securizar nuestra API con JWT.
Las librerías son System.IdentityModel.Tokens.Jwt y Microsoft.AspNetCore.Authentication.JwtBearer.
Añade ambas librerías a tu proyecto a través de NuGet Package Manager o mediante el archivo csproj.
¿Qué es el token Bearer?
El token Bearer es un tipo de token de autenticación utilizado en el protocolo HTTP.
El nombre "Bearer" se deriva de la forma en que se utiliza el token en las solicitudes HTTP, donde el token se "porta" (bearer en inglés) en el encabezado de autorización de la solicitud.
¿Cómo funciona el token Bearer?
- Generación del Token: Un servidor de autenticación o autorización emite un token de acceso después de que un usuario o una aplicación cliente se autentica correctamente.
Inclusión del Token en la Solicitud: El cliente incluye el token de acceso en la solicitud HTTP que envía al servidor de recursos.
Esto se hace en el encabezado de autorización de la solicitud HTTP, que se ve así:
Authorization: Bearer {token}
Donde
{token}
es el valor del token de acceso.Validación del Token: El servidor de recursos recibe la solicitud y extrae el token de acceso del encabezado de autorización.
A continuación, el servidor valida la autenticidad del token y verifica si el usuario o la aplicación cliente tienen permiso para acceder al recurso solicitado.
Ello implica comprobar la firma del token (en el caso de JWT), verificar la expiración y realizar otras comprobaciones de seguridad.
Respuesta del Servidor: Si el token de acceso es válido el servidor de recursos responde a la solicitud y proporciona acceso al recurso solicitado.
En caso contrario, se devuelve una respuesta de error (normalmente un código de estado HTTP 401 Unauthorized o 403 Forbidden).
Es importantísimo mantener la seguridad del token, ya que cualquier persona que tenga acceso al token puede utilizarlo para acceder a los recursos protegidos.
Utilizar técnicas como HTTPS para asegurar que las solicitudes y respuestas que contienen tokens estén cifradas y protegidas contra ataques de intermediarios.
¡Al código en C#! 👨💻
En mis APIs, bueno, en mis proyectos me gusta tenerlo todo segmentado por clases para poder reutilizar el mayor código posible, ya sabes DRY (don´t repeat yourself).
Por eso, los pasos que voy a seguir para integrar JWT en mi proyecto son:
- Configuración del JWT en el fichero principal del proyecto Program.cs
- Creación del modelo para la recepción de las credenciales que llamaré UserAuthModel.cs
- Programación de una nueva clase que llamaré Auth.cs
- Creación del filtro que llamaré CheckToken.cs y heredará de IEndpointFilter para reutilizar en todos mis endpoints.
- Integración con Swagger
Configuración de JWT en el fichero principal del proyecto Program.cs
En mis APIs, después de añadir una lista blanca de dominios permitidos para el CORS, añado la configuración para JWT como sigue:
builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Auth.JWT_SECRET_KEY)), ValidateIssuerSigningKey = true, ValidateIssuer = true, ValidIssuer= Auth.JWT_ISSUER, ValidateAudience = true, ValidAudience = Auth.JWT_AUDIENCE, ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; options.SaveToken = true; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseCors("MyAllowedOrigins"); app.UseAuthentication(); app.UseAuthorization();
Creación del modelo para la recepción de las credenciales que llamaré UserAuthModel.cs
public class UserAuthModel { public required string Email { get; set; } public required string Password { get; set; } }
Programación de una nueva clase que llamaré Auth.cs encargada de generar el token y almacenar los datos necesarios para generarlo
En la clase Auth.cs almacenaré los datos para generar el token, datos como la secret key, el tiempo de validez...
También tendré el método para generar el token
public static class Auth { public readonly static string JWT_SECRET_KEY = "YOUR_SECRET_KEY"; private readonly static int HOURS_TOKEN_EXPIRE = 1; public readonly static string JWT_AUDIENCE = "https://localhost:7017"; public readonly static string JWT_ISSUER = "https://localhost:7017"; public static string GetToken(string email) { try { // Crear los claims del token var claims = new[] { new Claim(ClaimTypes.Name, email) // Puedes agregar más claims según tus necesidades }; // Crear el token var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.UTF8.GetBytes(Auth.JWT_SECRET_KEY); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.AddHours(HOURS_TOKEN_EXPIRE), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), Audience = JWT_AUDIENCE, Issuer = JWT_ISSUER }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); return tokenString; } catch (Exception ex) { throw new Exception(ex.Message); } } public static bool VerifyCredentials(string email, string password) { //Aquí añades tu lógica para verificar usuario y contraseña //Si todo es correcto devuelves true, si no, devuelvas false } }
Creación del filtro que llamaré CheckToken.cs y heredará de IEndpointFilter para reutilizar en todos mis endpoints.
public class CheckToken : IEndpointFilter { public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { string token = context.HttpContext.Request.Headers.Authorization.ToString(); if (token=="") { return Results.Json(new { message = "Falta el token" },statusCode:401); } try { string tokenWithoutBearer = token.Replace("Bearer ", ""); var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.UTF8.GetBytes(Auth.JWT_SECRET_KEY); var tokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidIssuer = JWT_ISSUER, ValidAudience = JWT_AUDIENCE, IssuerSigningKey = new SymmetricSecurityKey(key) }; try { SecurityToken validatedToken; var principal = tokenHandler.ValidateToken(tokenWithoutBearer, tokenValidationParameters, out validatedToken); // En este punto, el token es válido y has obtenido un ClaimsPrincipal // Puedes acceder a los claims dentro del principal, por ejemplo: principal.Claims return await next(context); //continúa al endpoint } catch (SecurityTokenException e) { // El token no es válido Console.WriteLine(e.Message); return Results.Json(new { message = e.Message }, statusCode: 401); } } catch(Exception ex) { return Results.Json(new { message = ex.Message }, statusCode: 401); } } }
Añadiendo filtros a los endpoints para securizarlos.
Es importante que para el endpoint de login añadas el método AllowAnonymous(), si securizas el login nunca podrás loguearte, creo que se entiende 🤡
app.MapPost("/login", ([FromBody] UserAuthModel loginData) => { try { if(loginData == null || loginData.Email==null || loginData.Password==null) { Results.StatusCode(400); return Results.Text("Faltan los datos para la autenticación"); } var email = loginData.Email; var password = loginData.Password; // Verificar las credenciales del usuario bool isValid = Auth.VerifyCredentials(email, password); if (isValid) { string tokenString = Auth.GetToken(email); return Results.Ok(new { token = tokenString }); } return Results.Unauthorized(); } catch(Exception e) { return Results.Problem(e.Message); } }).AllowAnonymous();👈 app.MapGet("/dataBaseIsAlive", () => { try { if (bdd.GetStatusConnection()) { return Results.Ok(); } return Results.Problem("No se pudo conectar"); } catch (Exception e) { return Results.Problem(e.Message); } }).AddEndpointFilter<CheckToken>();👈 // El resto de endpoints...
Integrando autorización en Swagger y minimal API
Para integrar el tema de la securización en Swagger y no tener que estar utilizando Postman ni ningún software del estilo tienes que añadir al fichero principal Program.cs después de lo explicado en el punto 4.1 lo siguiente:
// Aprendes más acerca de la configuración de Swagger/OpenAPI en https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { // Configuración Swagger // Añade la información de seguridad requerida por Swagger c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); // Añade el esquema de seguridad a la operación requerida c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }); });
Haciendo login con Swagger y JWT en minimal API
Cuando estemos depurando nuestra API, se nos abrirá en el explorador la página de Swagger con nuestros endpoints.
Hacemos clic en nuestro login y posteriormente en el botón de Try it out
A continuación insertamos nuestras credenciales y pulsamos Execute
Si todo ha ido bien nos devolverá una respuesta satisfactoria (200) y el token.
Haciendo uso del token
Te explico cómo usar el token que hemos obtenido en el punto anterior.
Si te fijas en la parte derecha hay un botón que pone Authorize y tiene un icono con un candado abierto.
Eso significa que no estás aún autenticado.
Haz clic en el botón Authorize
En el cuadro de diálogo pegarás el token generado en el punto anterior.
Si todo va bien, verás que el icono del botón Authorize ha cambiado a un candado cerrado, significa que estamos autorizados.
Ahora, al hacer solicitudes a endpoints protegidos nos responderá satisfactoriamente.
Fichero Program.cs completo
Por si te queda alguna duda, todo el fichero Program.cs a continuación:
using GreenPortBackend; using GreenPortBackend.models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using System.Text; string[] OriginWhiteList = { "dominio1", "dominio2" }; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddCors(options => { options.AddPolicy("MyAllowedOrigins", policy => { policy.WithOrigins(OriginWhiteList) // note the port is included .AllowAnyHeader() .AllowAnyMethod(); }); }); // Add JWT configuration builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Auth.JWT_SECRET_KEY)), ValidateIssuerSigningKey = true, ValidateIssuer = true, //ValidIssuer= Auth.JWT_ISSUER, ValidateAudience = true, //ValidAudience = Auth.JWT_AUDIENCE, ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; options.SaveToken = true; }); builder.Services.AddAuthorization(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { // Configuración Swagger // Añade la información de seguridad requerida por Swagger c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); // Añade el esquema de seguridad a la operación requerida c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }); }); var app = builder.Build(); app.UseCors("MyAllowedOrigins"); app.UseAuthentication(); app.UseAuthorization(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.MapPost("/login", ([FromBody] UserAuthModel loginData) => { try { if(loginData == null || loginData.Email==null || loginData.Password==null) { Results.StatusCode(400); return Results.Text("Faltan los datos para la autenticación"); } var email = loginData.Email; var password = loginData.Password; // Verificar las credenciales del usuario bool isValid = Auth.VerifyCredentials(email, password); if (isValid) { string tokenString = Auth.GetToken(email); return Results.Ok(new { token = tokenString }); } return Results.Unauthorized(); } catch(Exception e) { return Results.Problem(e.Message); } }).AllowAnonymous(); app.MapGet("/dataBaseIsAlive", () => { try { if (bdd.GetStatusConnection()) { return Results.Ok(); } return Results.Problem("No se pudo conectar"); } catch (Exception e) { return Results.Problem(e.Message); } }).AddEndpointFilter<CheckToken>(); //resto de endpoints... app.Run();
Hasta luego 🖖