En cualquier aplicación, la seguridad es básica. La seguridad abarca tres áreas clave:
- Cifrado del tráfico de red para evitar que los hackers malintencionados lo intercepten.
- Autenticación de clientes y servidores para establecer identidad y confianza.
- Autorizar a los clientes para controlar el acceso a los sistemas y aplicar permisos basados en la identidad.
En este artículo vamos a tratar de abarcar la autenticación y la autorización.
Autenticación y Autorización
Responsabilidades de un middleware de autenticación Qué debe hacer un sistema de autenticación?
Desde un punto de vista técnico, autenticar es dejar en IPrincipal una identidad que contenga la propiedad IsAuthenticated a true
Para obtener esta identidad (en general un objeto ClaimsIdentity) el middleware de autenticación debe tener determinados datos en la petición que mande el cliente. Esos datos pueden venir a través de una cookie, un token o cualquier otro método. Si existe este dato y es correcto el middleware puede crear la identidad y autenticarla.
Pero autenticar iría un paso más allá. Podemos autenticar un usuario a través de muchas formas:
- Cookies, token
- Azure Active Directory (AAD)
- Cualquier otro proveedor oAuth o OpenID connect
Esto nos lleva a preguntar si el sistema de autenticación debe gestionar de forma automática la gestión de redirigir las llamadas a los proveedores indicados, y gestionar la callback de retorno para procesar la respuesta. La respuesta sería sí.
Por otra parte, el proceso debe decidir si inicia o no el proceso de autenticación de forma automática cuando recibe un 401, en cualquier método que requiera autorización:
Por tanto tenemos dos acciones que el pipeline de autenticación debería decidir hacer o no:
- Leer la información de la request y crear la identidad en el contexto (autenticar la petición)
- Iniciar un proceso de autenticación al recibir un 401 (iniciar el challenge de autenticación)
Esas dos propiedades son establecidas al registrar el middleware de autenticación en el método Configure del Startup:
[scss]app.UseCookieAuthentication(opt =>
{ opt.AutomaticAuthenticate = true; opt.AutomaticChallenge = true;
});
[/scss]
Aunque ASP.NET Core permite autenticación a través de WS-Federation para conseguir tokens mediante Azure Active Directory, en este artículo vamos a centrarnos en los tokens de portador JWT, ya que son más fáciles de desarrollar que con WS-Federation.
Añadir Autenticación/Autorización al servidor
Lo primero que debemos hacer para poder autenticar mediante tokens JWT es añadir el paquete correspondiente, Microsoft.AspNetCore.Authentication.JwtBearer
Agregamos el servicio de Autenticación en el método ConfigureServices
[scss]services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters {
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = JwtHelper.SecurityKey
};
});
[/scss]
La propiedad IssuerSigningKey requiere una implementación que devuelva un objeto de tipo Microsoft.IdentityModels.Tokens.SecurityKey con los datos necesarios para validar los tokens firmados. Una implementación simple sería la siguiente: new SymmetricSecurityKey(_tokenKey);
…donde _tokenKey, sería una clave que debería estar protegida, por ejemplo almacenando en un servidor de secretos como puede ser Azure Key Vault o similar. Este token se utilizará para generar los tokens de autenticación que utilizarán tanto los clientes que accedan al servicio, como el servidor para la verificación de la validez de los tokens recibidos en las peticiones. Por este motivo de alguna forma debe ser un dato compartido entre ambos.
Para esto sería buena práctica almacenaran este token de forma segura en un servidor de secretos, como Azure Key Vault.
Debemos añadir también el servicio de autorización:
[scss]services.AddAuthorization(options =>
{
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireClaim(ClaimTypes.Name);
});
});
[/scss]
Como se puede observar, al configurar el servicio de Autorización, (en este caso) se le ha establecido una restricción, mediante la cual, se va a exigir el claim del Name, en el token proporcionado en las llamadas de cliente. Si este claim no va incluido en el token, se devolverá un error 403 – Permission Denied
Esta restricción se aplicará siempre y cuando se especifique en la autorización del servicio la política creada:
[scss]namespace GrpcServiceServer.Services
{
[Authorize(policy: JwtBearerDefaults.AuthenticationScheme)] public class HelloCommunityService : GrpcServiceServer.HelloCommunityService.HelloCommunityServiceBase
{
…
… }
[/scss]
Una vez configurado todo, añadiremos el middleware de autorización y autenticación:
[scss]app.UseAuthentication(); app.UseAuthorization();
[/scss]
Por último añadiríamos el Atributo [Authorize] a los controladores que deseemos aplicar configuración de Autorización
[scss][Authorize]
public class HelloCommunityService : GrpcServiceServer.HelloCommunityService.HelloCommunityServiceBase
{
private readonly ILogger<HelloCommunityService> _logger; public HelloCommunityService(ILogger<HelloCommunityService> logger) {
_logger = logger;
}
public override Task<RequestReply> SayHello(RequestMessage request, ServerCallContext context)
{
return Task.FromResult(new RequestReply
{
Message = «Hello » + request.Name
});
}
…
…
[/scss]
Credenciales de Cliente
Una vez configurado el lado del servidor para requerir Autenticación/Autorización, al realizar las llamadas desde el cliente, obtendremos un error 401 Unauthenticated
Por esta razón, deberíamos añadir al cliente las cabeceras necesarias para el envío del token con la información requerida para que el servidor nos conozca y permita ejecutar los métodos deseados.
Esto podríamos hacerlo de varias formas, bien añadiendo en la llamada al servicio los headers necesarios, bien añadiendo mediante un interceptor al establecer la configuración inicial del servicio como se muestra a continuación:
[scss]var token = await Authenticate(«Alice»);
var channel = GrpcChannel.ForAddress(serverBase, new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(new SslCredentials(), CallCredentials.FromInterceptor((context, metad
{ metadata.Add(«Authorization», $»Bearer {token}»); return Task.CompletedTask;
})) });
var client = new HelloCommunityService.HelloCommunityServiceClient(channel);
[/scss]
En este punto el cliente de nuevo recibirá la respuesta esperada del servidor, ya que estaríamos autenticados y con los permisos necesarios para ejecutar los métodos deseados.