Protegiendo API con JWT.
Antes de leer el artículo, por favor, lea aquí
Código disponible GitHub.
Introducción y primeros pasos
Una vez concluido los pasos descritos en el artículo de Creando API con ASP.NET estamos en condiciones de proteger el acceso a la API. (Para tener un mejor entendimiento de que es JWT puedes leer el artículo AUTENTICACIÓN JWT, ¿QUÉ ES Y CUÁNDO USARLA?)
El primer paso es agregar el paquete que agregara todo lo necesario para trabajar con JWT en nuestra aplicación. En la consola del administrador de paquetes escribimos:
1 |
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer |
El orden de pasos es el siguiente:
- Debemos especificar una clave para el cifrado de la firma.
- Agregar en el middleware el servicio de JWT Bearer.
- Crear el modelo del Usuario
- Crear modelos de request y response
- Crear el controlador que realizará todo el proceso de autenticación
- Restringiendo acceso a las acciones y controladores
Clave para el cifrado de la firma
La clave para el cifrado de la firma la guardaremos en el fichero appsettings.json que se encuentra en la raíz del proyecto:
1 2 3 4 |
"AppSettings": { "Token": "KC7aglN6vciV576Cd5RmX24hDCIRPG9iRT2JLOxPbhFmDlPJr9vhVFb4LYzdJI03" }, //Esta clave es de ejemplo, ustedes deben generar para cada aplicación una clave con 32 caractéres o más |
Configurar el Middleware
El próximo paso es configurar el middleware, en el fichero Program.cs en la raíz del proyecto agregamos las siguientes líneas, antes var app = builder.Build();
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//JWT builder.Services.AddAuthentication().AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, ValidateAudience = false, ValidateIssuer = false, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( builder.Configuration.GetSection("AppSettings:token").Value!)) }; }); //</JWT> |
Modelo Usuario
En la carpeta Datos creamos una clase:
1 2 3 4 5 6 7 8 |
[Table("Usuarios")] public class Usuario { [Key] public Guid Id { get; set; } = Guid.NewGuid(); [Required] public string NombreUsuario { get; set; } = string.Empty; [Required] public string Password { get; set; } = string.Empty; } |
Agregamos este modelo al contexto para generar la tabla en nuestra base de datos. En el fichero ApplicationDbContext.cs dentro de la misma carpeta agregamos la propiedad:
1 |
public DbSet<Usuario> Usuarios { get; set; } |
En la consola de administrador de paquetes escribimos:
1 2 |
add-migration CreandoUsuarios update-database |
Si usamos el Explorador de objetos de SQL Server podemos verificar que la tabla ha sido creada con éxito:

Modelos de request y response
Por una elección personal, nos gusta crear estos modelos para organizar los formatos de los pedidos y las respuestas de las API.
En el caso de la autenticación crearemos dos Request y dos Responses(para registro y para login)
En la carpeta Datos creamos 4 clases: LoginRequest.cs, RegisterRequest.cs, LoginResponse.cs y RegisterResponse.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class LoginRequest { public string NombreUsuario { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } public class RegisterRequest { public string NombreUsuario { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; public DateTime Creado { get; set; } = DateTime.Now; } public class LoginResponse { public Guid UsuarioId { get; set; } = Guid.Empty; public string JwtToken { get; set; } = string.Empty; } public class RegisterResponse { public Guid Id { get; set; } = Guid.Empty; public string NombreUsuario { get; set; } = string.Empty; } |
Creando controlador de autenticación
En la carpeta Controllers creamos un nuevo controlador API llamado AuthController.cs.
Declaramos dos variables que inicializaremos en el constructor, estas nos permitirán leer, acceso al contexto para consultar la base de datos y poder leer el fichero appsettings.json para tener acceso a la clave para firmar el token.
1 2 3 4 5 6 7 8 9 |
private readonly IConfiguration _configuration; private readonly ApplicationDbContext _context; public AuthController(ApplicationDbContext context, IConfiguration configuration) { _context = context; _configuration = configuration; } |
Este controlador tendrá dos métodos auxiliares a las acciones, CrearToken y GetSha1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
private string GetSha1(string value) { var data = Encoding.ASCII.GetBytes(value); var hashData = SHA1.HashData(data); var hash = string.Empty; foreach (var b in hashData) hash += b.ToString("X2"); return hash; } //Recibe un objeto Usuario como parámetro private string CrearToken(Usuario usuario) { //se crea una lista de Claims var claims = new List<Claim> { //usamos en este ejemplo solo el nombre de usuario y el rol estático en este ejemplo new(ClaimTypes.Name, usuario.NombreUsuario), new(ClaimTypes.Role, "Admin") }; //Se cifra la firma, y se crea el JWT var key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_configuration.GetSection("AppSettings:Token").Value!)); var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); var token = new JwtSecurityToken( claims: claims, expires: DateTime.Now.AddDays(1), signingCredentials: cred ); var jwt = new JwtSecurityTokenHandler().WriteToken(token); //Retornamos el token como un string return jwt; } |
Ahora solo nos queda crear dos acciones, una para autenticar y otra para registrar(En el caso de este ejemplo solo mostraremos estas acciones):
Autenticar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[HttpPost("autenticar")] //recibe un objeto de LoginRequest(Usuario y contraseña) public async Task<ActionResult<LoginResponse>> Autenticar(LoginRequest loginRequest) { //Valida si los datos no vienen vacíos if (string.IsNullOrEmpty(loginRequest.NombreUsuario) || string.IsNullOrEmpty(loginRequest.Password)) return Unauthorized("Nombre de usuario o contraseña vacía"); //Busca el usuario con esa contraseña en la base de datos var data = await _context.Usuarios.Where(w => w.NombreUsuario == loginRequest.NombreUsuario && w.Password == GetSha1(loginRequest.Password)) .FirstOrDefaultAsync(); if (data == null) return Unauthorized("Nombre de Usuario o contraseña incorrecta"); //creamos la respuesta var response = new LoginResponse { JwtToken = CrearToken(data), UsuarioId = data.Id }; //devolvemos return Ok(response); } |
Registro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[HttpPost("registro")] public async Task<ActionResult<RegisterResponse>> Registro(RegisterRequest registerRequest) { if (string.IsNullOrEmpty(registerRequest.NombreUsuario) || string.IsNullOrEmpty(registerRequest.Password)) return BadRequest("Nombre de usuario o contraseña vacía"); var usuario = new Usuario { NombreUsuario = registerRequest.NombreUsuario, Password = GetSha1(registerRequest.Password) }; await _context.Usuarios.AddAsync(usuario); await _context.SaveChangesAsync(); var response = new RegisterResponse { Id = usuario.Id, NombreUsuario = usuario.NombreUsuario }; return Ok(response); } |
El proceso de autenticación esta listo, vamos a probarlo, corremos la aplicación con f5
Usaremos el software Postman. Puedes descargarlo de aquí: https://www.postman.com/downloads/
Primero registramos nuestro primer usuario, ponemos la URL en el Postman y especificamos que el método es POST, en el Body, marcamos la opción RAW y el formato JSON. y en ese formato escribimos los datos del usuario a crear y presionamos Send
1 2 3 4 |
{ "nombreUsuario": "usuario1", "password": "Pass12345*" } |

Si todo sale bien recibiremos la respuesta con el Id del usuario creado y el nombre:

Probemos ahora la autenticación de ese usuario. En el Postman ponemos la URL referente a la autenticación, en el Body marcamos Raw y formato JSON y en ese formato ponemos los datos de la autenticación y presionamos Send
1 2 3 4 |
{ "nombreUsuario": "usuario1", "password": "Pass12345*" } |

El Postman nos devuelve:

Restringir acceso
Antes probemos crear una persona, usando las acciones creadas en el articulo Creando una API en ASP.NET.
En el Postman, la URL: https://localhost:<puerto>/api/personas/ usando el método Post y en Body seleccionamos Raw con formato JSON:
1 2 3 4 5 6 |
{ "ci": "12345678910", "nombre": "Juan", "primerApellido": "Perez", "segundoApellido": "Perez" } |

La respuesta (señalada en el cuadro) es 201 Created.
Para restringir la creación de personas a usuarios No Autenticados, solo basta con poner la etiqueta [Authorize] justo antes de la acción en el controlador:
1 2 3 4 5 6 7 |
// POST: api/Personas [Authorize] [HttpPost] public async Task<ActionResult<Persona>> PostPersona(Persona persona) { //código } |
Ahora si probamos crear una nueva persona:

ya la respuesta es 401 Unauthorized.
Para acceder a la acción de crear una nueva persona en Authorization escogemos el tipo Bearer Token, pegamos el token que devolvió la autenticación:

Damos Send:

Ya tenemos nuevamente respuesta 201 Created.
Conclusiones
La etiqueta [Authorize] va a actuar en dependencia del nivel que se ponga, por ejemplo si se pone encima del enunciado del controlador, restringirá el acceso a todas las acciones a usuarios autenticados:
1 2 3 4 5 6 7 |
[Route("api/[controller]")] [ApiController] [Authorize] public class PersonasController : ControllerBase { //código } |
Para, dentro de un controlador restringido completamente poner una acción de acceso sin autenticar, agregamos encima de la acción la etiqueta [AllowAnonymous]. Ejemplo el controlador de AuthController.cs debe ser restringido excepto el método Autenticar.
1 2 3 4 5 6 |
[HttpPost("autenticar")] [AllowAnonymous] public async Task<ActionResult<LoginResponse>> Autenticar(LoginRequest loginRequest) { //código } |
El acceso mediante roles y el consumo de la API con la autenticación lo haremos en otro artículo.