Seguridad en los servicios REST con .NET Core

Una forma común de manejar la seguridad en tus servicios, es mediante la seguridad basada en tokens. En el cual después de que el usuario inicia sesión el servidor regresa una cadena encriptada con los datos del usuario, este token se debe enviar en cada petición a los servicios rest, el servidor desencripta el token y valida que el usuario tenga los permisos necesarios para accerder a la información, una explicación de forma gráfica es la siguiente:

Funcionamiento de Json Web Token
  1. El usuario inicia sesión ya sea en una aplicación móvil o en navegador. Internamente se envía una petición POST con el usuario y contraseña del usuario.
  2. El servidor valida el usuario y contraseña enviados y genera un token, el cual es básicamente una cadena encriptada donde agrega información como el Id del usuario, los roles que tiene el usuario, y el tiempo en el cual es token es válido por ejemplo 1 hora, 2 horas, 1 día, una vez caducado el token el usuario debe volver a iniciar sesión o pedir una renovación del token.
  3. El navegador o la aplicación recibe el token y lo guarda. Se puede guardar el token en el local storage de la página si es una aplicación web o en tu aplicación móvil.
  4. El usuario consulta alguna información del sistema, como por ejemplo la lista de clientes. En el servicio GET con la petición para la lista de clientes, en el header se envia el token regresado por el servidor.
  5. El servidor válida el token si es válido y el usuario tiene permiso para consultar la información regresa la información, si no regresa un código de error (401) No autorizado.

Un token es una cadena codificada en base64 formada por 3 partes las cuales están separadas por un punto.

  1. Header: Indica el algoritmo y tipo de token
  2. Payload: Datos del usuario, caducidad del token, roles del usuario
  3. Signature: Incluye una llave secreta para validar el token

Para poder generar los tokens necesitamos:

  • LLave secreta: Es una llave que permite encriptar/desencriptar la información del token
  • Issuer: Es quien genera el token, por lo general es la URL del servidor que contiene los servicios

La información adicional que guardas en el token como el id del usuario se conoce como Claims, la lista de claims disponibles es:

En .NET Core implementar este tipo de seguridad es relativamente fácil.

Seguridad basada en roles

Una forma fácil de manejar la seguridad es mediante roles, por ejemplo para un sistema de ventas se pueden tener los siguientes grupos (roles) de usuarios:

  • Administradores: Pueden consultar y modificar cualquier información en el sistema
  • Vendedores: Se encargan de revisar los pedidos de los clientes y enviar los productos
  • Clientes: Solamente pueden comprar productos

De esta manera cuando un usuario inicia sesión aparte de regresar el id del usuario, regresas la lista de roles que tiene el usuario, asi en cada petición el servidor puede validar el rol que tiene el usuario en el token y si tiene el rol adecuado muestra la información.

  1. Agregamos el siguiente código en nuestro archivo Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//Codigo
services.AddAuthentication(JwtBearerDefaults
.AuthenticationScheme)
.AddJwtBearer(cfg => {
cfg.Audience = Configuration["Tokens:Issuer"];
cfg.Authority = Configuration["Tokens:Issuer"];
cfg.TokenValidationParameters = new
TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
Configuration["Tokens:Key"])
);
});
}
public void Configure(IApplicationBuilder app)        
{
app.UseAuthentication();
//Codigo
}

3. Agregamos lo siguiente en nuestro archivo appsettings.json, de esta manera podemos cambiar nuestra llave secreta sin tener que volver a instalar nuestra dll en el servidor.

{
"Tokens": {
"Key": "tullavesecreta",
"Issuer": "http://localhost:56962/"
},

En nuestro método login agregamos el código para generar y regresar el token, también le indicamos que este servicio permite el acceso anónimo, es decir que permite que este accesible sin realizar ninguna validaciónde seguridad

[HttpPost("Login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody]LoginDTO loginDTO)
{
JwtSecurityToken token;
DateTime expiration;
//Tu validación de usuario password
var llave = Encoding.UTF8.GetBytes(Config["Tokens:Key"])
var key = new SymmetricSecurityKey(llave);
var creds = new SigningCredentials(key,
SecurityAlgorithms.HmacSha256);
token = new JwtSecurityToken(Config["Tokens:Issuer"],
Config["Tokens:Issuer"],
claims,
expires: DateTime.Now.AddDays(30),
signingCredentials: creds);
var claims = new Claim[]
{
new Claim(ClaimTypes.Sid, usuario.Id.ToString()),
new Claim(ClaimTypes.Role, usuario.Rol),
new Claim("Empresa",usuario.Empresa.ToString())
};
tokenHandler = new JwtSecurityTokenHandler().WriteToken(token);
expiration = token.ValidTo;
}

En los métodos de los servicios donde vamos a restringir la información agregamos:

[HttpGet]
[Authorize(Roles = "Administrador, Ventas")]
public List<Categoria> GetCategoria()
{
return categoriaDAO.ObtenerTodo();
}

Seguridad basada en directivas

Puedes definir la seguridad de un servicio si cumple con un claim, por ejemplo el servicio esta disponible solo para los usuarios de la empresa 1 y 2.

  1. En nuestro archivo startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options => {
options.AddPolicy("Empresa", policy =>
policy.RequireClaim("Empresa", "1", "2")); });

2. En nuestra clase controller del método que deseamos restringir

public class ConfiguracionController : Controller 
{
[Authorize(Policy = "Empresa")]
public List<Configuracion> GetConfiguracion()
{
}
}

Si deseamos que todos los métodos de un servicio solamente estén disponibles para personas mayores de 18 años:

  1. En nuestro archivo startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("MayorEdad", policy =>
policy.Requirements.Add(new RequisitoEdadMinima(18)));
});
}

2. Crea una nueva clase RequisitoEdadMinima.cs que implemente las interfaces AuthorizationHandler y IAuthorizationRequirement.

Agrega tu código con tu validación personalizada. Para este ejemplo necesitamos obtener la fecha de nacimiento del usuario el cual previamente en el login se debe haber agregado como claim.

La forma de obtener un claim en código es

context.User.FindFirst(c => c.Type==ClaimTypes.DateOfBirth).Value);

Para regresar un mensaje de error y que se ejecute el servicio regresamos context.Succeed(requirement) en caso de que no tenga permiso regresamos Task.CompletedTask

public class RequisitoEdadMinima :  
AuthorizationHandler<MinimumAgeRequirement>,
IAuthorizationRequirement
{
int _edadMinima;
public RequisitoEdadMinima(int edadMinima)
{
_edadMinima = edadMinima;
}
    protected override Task HandleRequirementAsync(   
AuthorizationHandlerContext context,
RequisitoEdadMinima requirement)
{
//Se revisa que el token tenga el claim de fecha de
//nacimiento
if (!context.User.HasClaim(c => c.Type ==
ClaimTypes.DateOfBirth))
return Task.CompletedTask;
var fechaNacimiento = Convert.ToDateTime(
context.User.FindFirst(c => c.Type ==
ClaimTypes.DateOfBirth).Value);
int edad = DateTime.Today.AddYears(
(FechaNacimiento.Year*-1));
if (edad >= _edadMinima)
return context.Succeed(requirement);
return Task.CompletedTask;
}
}

3. Si agregas la restricción en la clase, aplica para todos los servicios del controlador.

[Authorize(Policy = "MayorEdad")] 
public class AlcoholController : Controller
{
//Codigo de los métodos GET, POST, PUT, DELETE
}

Seguridad Personalizada

Puedes crear tu propia seguridad, por ejemplo para validar si un usuario tiene el permiso de agregar registros. Deseas que la validación se realice al momento en que el usuario quiere consultar o modificar la información al sistema.

  1. Creamos una clase Operaciones.cs para definir las operaciones que vamos a realizar
public static class Operaciones
{
public static OperationAuthorizationRequirement Crear = new
OperationAuthorizationRequirement { Name = "Crear" };
public static OperationAuthorizationRequirement Consultar = new
OperationAuthorizationRequirement { Name = "Consultar" };
public static OperationAuthorizationRequirement Modificar = new
OperationAuthorizationRequirement { Name = "Modificar" };
public static OperationAuthorizationRequirement Borrar = new
OperationAuthorizationRequirement { Name = "Borrar" };
}

2. Creamos una clase PermisoDTO el cual contiene el nombre de la opción del sistema que queremos validar

public class PermisoDTO
{
public string Opcion { get; set; }
}

3. Agregamos una nueva clase PermisoEditHandler que hereda de la clase AuthorizarionHandler la cual recibe como parámetros genéricos un objeto de la clase OperationAuthorizationRequirement y le podemos pasar como parámetro adicional cualquier otro objeto con los datos que necesitamos para validar el permiso, en este caso agregamos nuesta clase PermisoDTO

public class PermisoEditHandler : 
AuthorizationHandler<OperationAuthorizationRequirement, PermisoDTO>
{
private BaseDatosContext contexto;
public PermisoEditHandler(IConfiguration config)
{
contexto = new BaseDatosContext(config);
}

protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement operacion,
PermisoDTO permiso)
{
Permiso seguridad = new Permiso(contexto);
if (!context.User.HasClaim(c => c.Type == ClaimTypes.Sid))
{
context.Succeed(operacion);
}
var usuarioId = Convert.ToInt32(context.User.FindFirst(c =>
c.Type == ClaimTypes.Sid).Value);
var permisoXUsuario = new PermisoXUsuario(contexto);
if (!permisoXUsuario.TienePermisoUsuario(
usuarioId, permiso.Pagina, operacion.Name);
return Task.CompletedTask;
context.Succeed(operacion);
}
}

3. En nuestra clase startup.cs agremos nuestra clase personalizada.

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAuthorizationHandler,
PermisoEditHandler>();
}

4. En el servicio que deseamos realizar esta validación

[HttpPost]
public async Task<IActionResult> PostAsync([FromBody]Empresa
empresa)
{
var authorizationResult = await
_authorizationService.AuthorizeAsync(User, permiso,
Operaciones.Crear);
if (authorizationResult.Succeeded)
{
//Tu codigo
}
else
{
return Forbid( "El usuario no tiene permiso para agregar,
verifique por favor... ")
}
}

Seguridad con filtros

Otra forma de realizar este tipo de validación mas personalizada la puedes realizar con una característica llamada Filtros, el cual se ejecuta antes de la llamada a cada servicio para validar que tenga permiso o al final de cada servicio para por ejemplo guardar los datos que realiza modificaciones al registro.

  1. Creamos una clase Operaciones.cs para definir las operaciones que vamos a realizar
public static class Operaciones
{
public static OperationAuthorizationRequirement Crear = new
OperationAuthorizationRequirement { Name = "Crear" };
public static OperationAuthorizationRequirement Consultar = new
OperationAuthorizationRequirement { Name = "Consultar" };
public static OperationAuthorizationRequirement Modificar = new
OperationAuthorizationRequirement { Name = "Modificar" };
public static OperationAuthorizationRequirement Borrar = new
OperationAuthorizationRequirement { Name = "Borrar" };
}

2. Creamos una clase PermisoDTO el cual contiene el nombre de la opción del sistema que queremos validar

public class PermisoDTO
{
public string Opcion { get; set; }
}

3. Creamos una clase Controller que será una clase de la cual todos nuestros contollers heredan. Esta clase tendrá una propiedad que nos indica nombre de la información que deseamos validar. Por ejemplo Ventas

public class BaseController : Controller
{
public PermisoDTO permiso;
}

4. Creamos una nueva clase llamada PermisoFilter que hereda de IActionFilter

public class PermisoFilter : IActionFilter
{
//Este código se manda llamar antes de cada servicio
public void OnActionExecuting(ActionExecutingContext context)
{
var controller = context.Controller as BaseController;
//Obtenemos el método del servicio que se va a ejecutar
string accion = context.RouteData.Values["action"]
.ToString();
string operacion = string.Empty;
switch (accion)
{
case "Post":
operacion = Operaciones.Crear.Name;
break;
case "Put":
operacion = Operaciones.Modificar.Name;
break;
case "Delete":
operacion = Operaciones.Borrar.Name;
break;
case "Get":
operacion = Operaciones.Consultar.Name;
break;
}
//Validamos que el token tenga el claim sid
if (!Usuario.HasClaim(c => c.Type == ClaimTypes.Sid))
{
return context.Result = new JsonResult(new
{
StatusCode = 403,
Value = "Token incorrecto",
ContentType = customError.ContentType
};
}
//Esta clase contiene la lógica para validar que el usuario
//tiene permiso para la opción
UsuarioDAO usuario = new UsuarioDAO();
if (!usuario.TienePermiso(usuarioId,
controller.permiso.Opcion,
operacion)
return context.Result = new JsonResult(new
{
StatusCode = 403,
Value = "El usuario no tiene permiso",
ContentType = customError.ContentType
};
}
    //Este código se manda llamar después de cada servicio
public void OnActionExecuted(ActionExecutedContext context)
{

}
}

3. Agregamos el filtro a nuestra clase controller y en el constructor asignamos el nombre de la opción que deseamos validar

[TypeFilter(typeof(PermisoFilter))]
public class ProductoController : BaseController
{
public ProductoController()
{
permiso.Opcion = "productos";
}
}

Puedes ver la documentación oficial de microsoft aquí

Si deseas aprender mas de .net core, servicios REST y Entity Framework puedes consultar mi gitbook gratuito aquí