En esta página

Autenticación y autorización con JWT en ASP.NET Core

14 min lectura TextoCap. 5 — Producción

Autenticación vs Autorización

  • Autenticación — ¿Quién eres? (¿El token es válido? ¿El usuario existe?)
  • Autorización — ¿Qué puedes hacer? (¿Tienes el rol/permiso necesario?)

En ASP.NET Core, ambos son middleware: UseAuthentication identifica al usuario, UseAuthorization verifica sus permisos.

JWT (JSON Web Token)

Un JWT es un token firmado con tres partes:

Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← Header (alg, tipo)
.eyJzdWIiOiIxMjM0NTY3ODkwIn0             ← Payload (claims)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature

El servidor firma el token con una clave secreta. El cliente incluye el token en cada petición:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Configurar JWT en appsettings

{
  "Jwt": {
    "Key": "superSecretKeyThatShouldBeAtLeast32CharactersLong!",
    "Issuer": "https://api.miapp.com",
    "Audience": "https://miapp.com",
    "ExpirationHours": 8
  }
}

Endpoint de login

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IUsuarioService _usuarios;
    private readonly TokenService _tokens;

    public AuthController(IUsuarioService usuarios, TokenService tokens)
    {
        _usuarios = usuarios;
        _tokens   = tokens;
    }

    [HttpPost("login")]
    public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest req)
    {
        // 1. Verificar credenciales
        var usuario = await _usuarios.VerificarCredencialesAsync(req.Email, req.Password);

        if (usuario is null)
            return Unauthorized(new { mensaje = "Credenciales inválidas" });

        // 2. Generar token JWT
        string token = _tokens.GenerarToken(usuario.Id, usuario.Email, usuario.Rol);

        return Ok(new LoginResponse(
            Token: token,
            ExpiresAt: DateTime.UtcNow.AddHours(8),
            Usuario: new UsuarioDto(usuario.Id, usuario.Email, usuario.Nombre)
        ));
    }

    [HttpPost("register")]
    public async Task<ActionResult<UsuarioDto>> Registrar([FromBody] RegistroRequest req)
    {
        if (await _usuarios.ExisteEmailAsync(req.Email))
            return Conflict(new { mensaje = "El email ya está registrado" });

        var usuario = await _usuarios.CrearAsync(req);
        return CreatedAtAction(nameof(Login), new UsuarioDto(usuario.Id, usuario.Email, usuario.Nombre));
    }
}

record LoginRequest(string Email, string Password);
record LoginResponse(string Token, DateTime ExpiresAt, UsuarioDto Usuario);
record RegistroRequest(string Nombre, string Email, string Password);
record UsuarioDto(int Id, string Email, string Nombre);

[Authorize] y sus variantes

// Requiere autenticación (cualquier usuario válido)
[Authorize]
public IActionResult MiPerfil() { ... }

// Requiere un rol específico
[Authorize(Roles = "Admin")]
public IActionResult PanelAdmin() { ... }

// Requiere uno de varios roles
[Authorize(Roles = "Admin,Manager")]
public IActionResult GestionEquipo() { ... }

// Requiere una política
[Authorize(Policy = "SoloJefes")]
public IActionResult ReporteEjecutivo() { ... }

// Permite acceso sin autenticación (sobreescribe [Authorize] del controlador)
[AllowAnonymous]
public IActionResult BienvenidaPublica() { ... }

Políticas de autorización

Para lógica de autorización compleja:

// Registrar políticas
builder.Services.AddAuthorization(opts =>
{
    // Política basada en claim
    opts.AddPolicy("TieneSuscripcionPremium", p =>
        p.RequireClaim("subscription", "premium", "enterprise"));

    // Política basada en múltiples requisitos
    opts.AddPolicy("PuedePubContener", p =>
    {
        p.RequireAuthenticatedUser();
        p.RequireRole("Editor", "Admin");
        p.RequireClaim("email_verified", "true");
    });

    // Política personalizada con IAuthorizationRequirement
    opts.AddPolicy("PropietarioOAdmin", p =>
        p.AddRequirements(new PropietarioOAdminRequirement()));
});

// Requisito de autorización personalizado
public class PropietarioOAdminRequirement : IAuthorizationRequirement { }

public class PropietarioOAdminHandler
    : AuthorizationHandler<PropietarioOAdminRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PropietarioOAdminRequirement requirement)
    {
        var userId    = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        var esAdmin   = context.User.IsInRole("Admin");

        // context.Resource tiene el objeto del endpoint (si se configura)
        if (esAdmin || /* lógica de propietario */)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

Leer los claims del usuario

[HttpGet("perfil")]
[Authorize]
public IActionResult ObtenerPerfil()
{
    // Acceder a los claims del token JWT
    var userId    = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var email     = User.FindFirstValue(ClaimTypes.Email);
    var rol       = User.FindFirstValue(ClaimTypes.Role);
    var jti       = User.FindFirstValue(JwtRegisteredClaimNames.Jti);

    // Verificar rol
    bool esAdmin  = User.IsInRole("Admin");

    // Verificar claim personalizado
    bool esPremium = User.HasClaim("subscription", "premium");

    return Ok(new { userId, email, rol, esAdmin, esPremium });
}

Refresh Tokens (patrón seguro)

// Los JWT de corta duración + refresh token son más seguros
public class RefreshTokenService
{
    private readonly Dictionary<string, RefreshToken> _tokens = new();

    public string Crear(int userId)
    {
        var token = new RefreshToken
        {
            Token     = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"),
            UserId    = userId,
            ExpiresAt = DateTime.UtcNow.AddDays(30),
            CreadoEn  = DateTime.UtcNow
        };
        _tokens[token.Token] = token;
        return token.Token;
    }

    public RefreshToken? Validar(string token)
    {
        if (!_tokens.TryGetValue(token, out var rt)) return null;
        if (rt.ExpiresAt < DateTime.UtcNow || rt.Revocado) return null;
        return rt;
    }

    public void Revocar(string token)
    {
        if (_tokens.TryGetValue(token, out var rt))
            rt.Revocado = true;
    }
}

public class RefreshToken
{
    public string   Token     { get; set; } = string.Empty;
    public int      UserId    { get; set; }
    public DateTime ExpiresAt { get; set; }
    public DateTime CreadoEn  { get; set; }
    public bool     Revocado  { get; set; }
}

Práctica

  1. Login básico: Implementa un AuthController con POST /login que verifique credenciales hardcodeadas y devuelva un JWT válido por 1 hora.
  2. Endpoint protegido: Crea GET /api/privado con [Authorize] y GET /api/admin con [Authorize(Roles = "Admin")]. Prueba con Swagger enviando el token.
  3. Política personalizada: Define una política "ConEmail" que requiera que el claim email_verified sea "true". Aplícala a un endpoint y prueba.

En la siguiente lección aprenderemos a crear middleware personalizado y filtros de acción para agregar comportamiento transversal a la API.

Guarda Jwt:Key en variables de entorno
Nunca almacenes el secreto JWT en appsettings.json en texto plano para producción. Usa variables de entorno (ASPNETCORE_Jwt__Key=...), Azure Key Vault, AWS Secrets Manager o cualquier gestor de secretos. La clave debe tener al menos 32 caracteres para HMAC-SHA256.
Claims como datos del usuario en el token
Los claims son pares clave-valor dentro del JWT que identifican al usuario. Incluye solo la información necesaria (ID, email, rol) porque el token se envía en cada solicitud. Para datos sensibles que cambian frecuentemente (como permisos), valídalos contra la base de datos en cada petición.
UseAuthentication ANTES de UseAuthorization
El orden del middleware es crítico: app.UseAuthentication() DEBE ir antes que app.UseAuthorization(). Si los inviertes, la autorización no sabrá quién es el usuario y rechazará todas las solicitudes con 401 aunque el token sea válido.
// Program.cs — configuración completa de JWT
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// ── Autenticación JWT ────────────────────────────────
var jwtKey = builder.Configuration["Jwt:Key"]
    ?? throw new InvalidOperationException("Jwt:Key no configurada");

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opts =>
    {
        opts.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey         = new SymmetricSecurityKey(
                                           Encoding.UTF8.GetBytes(jwtKey)),
            ValidateIssuer           = true,
            ValidIssuer              = builder.Configuration["Jwt:Issuer"],
            ValidateAudience         = true,
            ValidAudience            = builder.Configuration["Jwt:Audience"],
            ValidateLifetime         = true,
            ClockSkew                = TimeSpan.Zero, // sin margen extra
        };
    });

// ── Autorización con políticas ───────────────────────
builder.Services.AddAuthorization(opts =>
{
    opts.AddPolicy("AdminOnly",
        p => p.RequireRole("Admin"));
    opts.AddPolicy("EditorOrAdmin",
        p => p.RequireRole("Editor", "Admin"));
    opts.AddPolicy("PremiumUser",
        p => p.RequireClaim("subscription", "premium"));
});

var app = builder.Build();
app.UseAuthentication(); // ANTES de Authorization
app.UseAuthorization();