0

Asp Net Core: Roles, Notificaciones y Directiva

En este artículo, examinaremos detenidamente la autorización en AspNet Core, considerando roles, notificaciones y directivas. ¿Cuándo sería pertinente utilizar cada uno y obtener una comprensión más precisa de su idoneidad? Así que, lo que explicaremos: Distinción entre Autenticación y Autorización, Exploración del concepto de autenticación, Exploración del concepto de autorización, Diversidad de enfoques en autorización, Conceptualización de rol, Conceptualización de reclamación, Conceptualización de póliza, Componentes clave, Código e implementaciones.

Authentication vs Authorization

Antes de ahondar demasiado en este tema, a pesar de que los términos suenan similares, la autenticación y la autorización constituyen pasos distintos en el proceso de inicio de sesión.

Authentication

Cualquier proceso de seguridad implica, en algún momento, la necesidad de autenticación. Por ejemplo, al iniciar sesión en su correo electrónico o desbloquear su teléfono, se lleva a cabo un procedimiento de autenticación. En este proceso, se le solicita proporcionar ciertas credenciales, como un nombre de usuario y contraseña, para que el sistema le otorgue acceso y le permita visualizar su información. Este paso es esencial para verificar la identidad del usuario y garantizar que solo las personas autorizadas accedan a la información protegida.

La autenticación puede adoptar muchas formas:

  • Contraseñas. Los nombres de usuario y las contraseñas **** son los factores de autenticación más comunes. Si un usuario introduce los datos correctos, el sistema asume que la identidad es válida y concede acceso.
  • Pines de un solo uso. Otorgue acceso para una sola sesión o transacción.
  • Aplicaciones de autenticación. Genere códigos de seguridad a través de una parte externa que otorgue acceso.
  • Biometría. Un usuario presenta una huella dactilar o un escaneo ocular para obtener acceso al sistema.

En determinadas situaciones, los sistemas requieren la confirmación adecuada de más de un factor antes de conceder acceso. Este requisito de autenticación multifactor (MFA) se implementa con frecuencia para fortalecer la seguridad más allá de lo que las contraseñas pueden proporcionar individualmente.

Authorization

Para comprender la autenticación, es esencial definirla de manera precisa, así como aclarar lo que no implica. En su esencia, la autenticación se refiere al proceso que determina las acciones que un usuario tiene el permiso de realizar.

En términos más simples, la autorización entra en juego para demostrar que un usuario tiene el derecho adecuado para realizar una solicitud específica. Comparémoslo con la experiencia de intentar acceder al área detrás del escenario en un concierto o evento. En este caso, no es necesario demostrar directamente tu identidad, sino mostrar el boleto, que actúa como prueba de que cuentas con el derecho necesario para ingresar al lugar.

Es fundamental destacar que la autorización opera de manera independiente a la autenticación, aunque depende de un mecanismo de autenticación para funcionar correctamente. Mientras la autenticación verifica la identidad del usuario, la autorización se centra en garantizar que dicho usuario tenga los privilegios adecuados para llevar a cabo determinadas acciones. Ambos conceptos son complementarios, formando una base sólida para establecer un sistema de seguridad efectivo.

Roles

Representan un conjunto de permisos para llevar a cabo actividades específicas en la aplicación. Podemos conceptualizar un rol como un booleano, indicando si poseemos o no dicho rol, con valores verdadero o falso.

Entonces, lo que efectuamos con los roles es asociar funcionalidad a un rol y, una vez que asignamos un usuario a un rol, ese conjunto de funcionalidades se establece para el usuario. Una vez que eliminamos el rol, estas funcionalidades se eliminan.

Un rol protegerá el acceso a la función; sin el rol adecuado, el usuario no podrá ejecutar esa función.

Claims

Son completamente diferentes de los roles, los basados en reclamaciones son más flexibles que los roles, son un par de valores clave. La notificación pertenece a un usuario o una entidad y la notificación se usa para describir al usuario o la entidad. Las notificaciones son esencialmente propiedades del usuario e informan a la autorización sobre el usuario.

Podemos observar en la licencia de conducir que hay varios claims acerca de este liceo, lo que esencialmente denota que hay 6 fragmentos de información sobre el usuario. Así que, si buscamos trasladar esto a una estructura basada en código, se verá algo como esto.

{
    "dl":"123456789",
    "exp":"07/11/2025",
    "ln":"DOE",
    "fn":"John",
  "dob":"09/05/1993",
  "sex":"M",
    "hair":"brn",
    "eyes":"blue",
    "hgt":"6.0"
    "wgt":"183lb",
  "class":"C"
}

Por ende, estas afirmaciones se proporcionarán al usuario una vez que inicie sesión.

Las afirmaciones pueden operar junto con roles o prescindir de ellos, dependiendo de cómo deseemos llevar a cabo la implementación del proceso de autorización.

Policy

Son funciones o reglas que se utilizan para examinar la información del usuario y determinar si se concede o niega el permiso.

Políticas que básicamente se inician con el contexto, comparando al usuario con una lista de políticas y, en función de la lista, otorgarán o denegarán permiso al recurso solicitado.

Requisitos para el uso de la autenticación basada en roles y la autorización basada en reclamaciones, con un controlador de requisitos y una política preconfigurada. La directiva consta de uno o más requisitos.

Roles vs Claims vs Policy

Un rol es una categoría simbólica que agrupa a los usuarios que comparten niveles de privilegios de seguridad similares. La autorización basada en roles implica identificar primero al usuario, luego determinar los roles a los que está asignado y, finalmente, comparar esos roles con los roles autorizados para acceder a un recurso.

Ejemplo

Luego de crear un nuevo proyecto en Visual Estudio, el primer paso implica la actualización de la clase de inicio para incorporar roles en nuestros proveedores de identidad. En el método ConfigureServices de la clase Startup, es esencial realizar las siguientes actualizaciones:

services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
                        .AddEntityFrameworkStores<ApiDbContext>();

Ahora, lo siguiente que debemos realizar es crear un controlador adicional llamado SetupController en el directorio de controladores y añadir lo siguiente.

[Route("api/[controller]")] // api/setup
    [ApiController]
    public class SetupController: ControllerBase
    {
        private readonly ApiDbContext _context;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly UserManager<IdentityUser> _userManager;
        protected readonly ILogger<SetupController> _logger;

        public SetupController(
            ApiDbContext context,
            RoleManager<IdentityRole> roleManager,
            UserManager<IdentityUser> userManager,
            ILogger<SetupController> logger)
        {   
            _logger = logger;
            _roleManager = roleManager;
            _userManager = userManager;
            _context = context;
        }

        [HttpGet]
        public IActionResult GetAllRoles()
        {
            var roles = _roleManager.Roles.ToList();
            return Ok(roles);
        }

        [HttpPost]
        public async Task<IActionResult> CreateRole(string roleName)
        {
            var roleExist = await _roleManager.RoleExistsAsync(roleName);
            if (!roleExist) {
                //create the roles and seed them to the database: Question 1
                var roleResult = await _roleManager.CreateAsync (new IdentityRole (roleName));

                if (roleResult.Succeeded) {
                    _logger.LogInformation (1, "Roles Added");
                    return Ok(new {result = $"Role {roleName} added successfully"});
                } else {
                    _logger.LogInformation (2, "Error");
                    return BadRequest(new {error = $"Issue adding the new {roleName} role"});
                }
            }

            return BadRequest(new {error = "Role already exist"});
        }

        // Get all users
        [HttpGet]
        [Route("GetAllUsers")]
        public async Task<IActionResult> GetAllUsers()
        {
            var users = await _userManager.Users.ToListAsync();
            return Ok(users);
        }

        // Add User to role
        [HttpPost]
        [Route("AddUserToRole")]
        public async Task<IActionResult> AddUserToRole(string email, string roleName)
        {
            var user = await _userManager.FindByEmailAsync(email);

            if(user != null)
            {
                var result = await _userManager.AddToRoleAsync(user, roleName);

                if(result.Succeeded)
                {
                    _logger.LogInformation (1, $"User {user.Email} added to the {roleName} role");
                    return Ok(new {result = $"User {user.Email} added to the {roleName} role"});
                }
                else
                {
                    _logger.LogInformation (1, $"Error: Unable to add user {user.Email} to the {roleName} role");
                    return BadRequest(new {error = $"Error: Unable to add user {user.Email} to the {roleName} role"});
                }
            }

            // User doesn't exist
            return BadRequest(new {error = "Unable to find user"});
        }

        // Get specific user role
        [HttpGet]
        [Route("GetUserRoles")]
        public async Task<IActionResult> GetUserRoles(string email)
        {
            // Resolve the user via their email
            var user = await _userManager.FindByEmailAsync(email);
            // Get the roles for the user
            var roles = await _userManager.GetRolesAsync(user);
            return Ok(roles);
        }

        // Remove User to role
        [HttpPost]
        [Route("RemoveUserFromRole")]
        public async Task<IActionResult> RemoveUserFromRole(string email, string roleName)
        {
            var user = await _userManager.FindByEmailAsync(email);

            if(user != null)
            {
                var result = await _userManager.RemoveFromRoleAsync(user, roleName);

                if(result.Succeeded)
                {
                    _logger.LogInformation (1, $"User {user.Email} removed from the {roleName} role");
                    return Ok(new {result = $"User {user.Email} removed from the {roleName} role"});
                }
                else
                {
                    _logger.LogInformation (1, $"Error: Unable to removed user {user.Email} from the {roleName} role");
                    return BadRequest(new {error = $"Error: Unable to removed user {user.Email} from the {roleName} role"});
                }
            }

            // User doesn't exist
            return BadRequest(new {error = "Unable to find user"});
        }
    }

Una vez que terminamos con el SetupController, pasemos al controlador de gestión de autenticación y actualicemos lo siguiente:

// We need to add the following before the constructor
private readonly RoleManager<IdentityRole> _roleManager;
protected readonly ILogger<AuthManagementController> _logger;

// We need to update the constructor to the following
public AuthManagementController(
          UserManager<IdentityUser> userManager,
          RoleManager<IdentityRole> roleManager,
          IOptionsMonitor<JwtConfig> optionsMonitor,
          TokenValidationParameters tokenValidationParams,
          ILogger<AuthManagementController> logger,
          ApiDbContext apiDbContext)
{
    _logger = logger;
    _userManager = userManager;
    _roleManager = roleManager;
    _jwtConfig = optionsMonitor.CurrentValue;
    _tokenValidationParams = tokenValidationParams;
    _apiDbContext = apiDbContext;
}

// We need to create a GetValidClaims method
private async Task<List<Claim>> GetValidClaims(IdentityUser user)
{
    IdentityOptions _options = new IdentityOptions();
    var claims = new List<Claim>
    {
        new Claim("Id", user.Id), 
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim(JwtRegisteredClaimNames.Sub, user.Email),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
        new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName),
    };

    var userClaims = await _userManager.GetClaimsAsync(user);
    var userRoles = await _userManager.GetRolesAsync(user);
    claims.AddRange(userClaims);
    foreach (var userRole in userRoles)
    {
        claims.Add(new Claim(ClaimTypes.Role, userRole));
        var role = await _roleManager.FindByNameAsync(userRole);
        if(role != null)
        {
            var roleClaims = await _roleManager.GetClaimsAsync(role);
            foreach(Claim roleClaim in roleClaims)
            {
                claims.Add(roleClaim);
            }
        }
    }
    return claims;
}

// We need to update the GenerateJwtToken method
var claims = await GetValidClaims(user);

var tokenDescriptor = new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity(claims),
    Expires = DateTime.UtcNow.AddMinutes(5), // 5-10 
    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

A continuación, debemos actualizar el atributo TodoController para agregarle los roles:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "AppUser")]

En este momento, procedemos a agregar nuestro Controlador ClaimSetup; en la carpeta de controladores, crearemos una nueva clase denominada ClaimSetupController e incluiremos lo siguiente.

[Route("api/[controller]")] // api/ClaimSetup
[ApiController]
public class ClaimSetupController : ControllerBase
{
    private readonly ApiDbContext _context;
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly UserManager<IdentityUser> _userManager;
    protected readonly ILogger<ClaimSetupController> _logger;

    public ClaimSetupController(
        ApiDbContext context,
        RoleManager<IdentityRole> roleManager,
        UserManager<IdentityUser> userManager,
        ILogger<ClaimSetupController> logger)
    {   
        _logger = logger;
        _roleManager = roleManager;
        _userManager = userManager;
        _context = context;
    }

    [HttpGet]
    public async Task<IActionResult> GetAllClaims(string email)
    {
        var user = await _userManager.FindByEmailAsync(email);

        var claims = await _userManager.GetClaimsAsync(user);

        return Ok(claims);
    }

    // Add Claim to user
    [HttpPost]
    [Route("AddClaimToUser")]
    public async Task<IActionResult> AddClaimToUser(string email, string claimName, string value)
    {
        var user = await _userManager.FindByEmailAsync(email);

        var userClaim = new Claim(claimName, value);

        if(user != null)
        {
            var result = await _userManager.AddClaimAsync(user, userClaim);

            if(result.Succeeded)
            {
                _logger.LogInformation (1, $"the claim {claimName} add to the  User {user.Email}");
                return Ok(new {result = $"the claim {claimName} add to the  User {user.Email}"});
            }
            else
            {
                _logger.LogInformation (1, $"Error: Unable to add the claim {claimName} to the  User {user.Email}");
                return BadRequest(new {error = $"Error: Unable to add the claim {claimName} to the  User {user.Email}"});
            }
        }

        // User doesn't exist
        return BadRequest(new {error = "Unable to find user"});
    }
}

Ahora necesitamos actualizar la clase Startup para crear una política de notificaciones, dentro de la Startup.cs en el directorio raíz debemos agregar lo siguiente en el método ConfigureServices.

services.AddAuthorization(options =>
  {
      options.AddPolicy("ViewItemsPolicy",
          policy => policy.RequireClaim("ViewItems"));
  });

A continuación, debemos actualizar el TodoController con lo siguiente en cualquier acción que queramos:

[HttpGet]
[Authorize(Policy = "ViewItemsPolicy")]
public async Task<IActionResult> GetItems()
{
    var items = await _context.Items.ToListAsync();
    return Ok(items);
}

Conclusiones

La autenticación garantiza la identidad del usuario, mientras que la autorización, roles, claims y policies son pilares cruciales en la seguridad. Definir roles permite asignar privilegios, los claims aportan información adicional, y las policies facilitan la gestión de accesos. Estos elementos forman un robusto sistema de control, asegurando que los usuarios accedan solo a lo necesario. La correcta implementación de autenticación y autorización fortalece la protección de datos, previene accesos no autorizados y contribuye a la integridad y confianza de los sistemas informáticos. En conjunto, son esenciales para un entorno digital seguro y eficiente.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *