Securizando Aplicaciones Web y Microservicios .NET
A menudo es necesario que los recursos y las APIs expuestas por un servicio se limiten a ciertos usuarios o clientes de confianza. El primer paso para tomar este tipo de decisiones de confianza a nivel de API es la autenticación. La autenticación es el proceso que consiste en determinar de forma fiable la identidad del usuario.
En los escenarios de microservicio, la autenticación suele gestionarse de forma centralizada. Si está utilizando una API Gateway, éste es un buen lugar para autenticarse, como se muestra en la Figura 9-1. Si utiliza este enfoque, asegúrese de que los microservicios individuales no pueden ser contactados directamente (sin la API Gateway) a menos que haya algún sistema de seguridad adicional para autenticar los mensajes, independientemente de que provengan o no del gateway.
Figura 9-1. Autenticación centralizada con un API Gateway
Si se puede acceder directamente a los servicios, puede utilizar un servicio de autenticación como Azure Active Directory o un microservicio de autenticación dedicado, que actúe como un servicio de token de seguridad (STS) para autenticar a los usuarios. Las decisiones de confianza se comparten entre servicios con tokens de seguridad o cookies. (Estas pueden ser compartidas entre aplicaciones, si es necesario, en ASP. NET Core con los servicios de protección de datos. Este patrón se ilustra en la Figura 9-2.
Figura 9-2. Autenticación por microservicio de identidad, se verifica la confiaza usando un token de autorización
El mecanismo principal en ASP. NET Core para identificar a los usuarios de una aplicación es el sistema de membresía ASP. NET Core Identity. ASP. NET Core Identity almacena la información del usuario (incluyendo información de inicio de sesión, roles y afirmaciones - claims) en un almacenamiento de datos configurado por el desarrollador. Típicamente, el ASP. NET Core Identity data store es un almacenamiento provisto por Entity Framework en el paquete Microsoft.AspNetCore.Identity.EntityFrameworkCore. Sin embargo, también se pueden usar almacenamientos personalizados o paquetes de terceros, para almacenar información de identidad en Azure Table Storage, DocumentDB u otras ubicaciones.
El siguiente código se toma de la plantilla del proyecto ASP. NET Core Web Application con la autenticación de cuenta de usuario individual seleccionada. Muestra cómo configurar ASP.NET Core Identity usando Entity Framework Core en el método Startup.ConfigureServices.
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); |
Una vez que se configura ASP.NET Core Identity, se habilita llamando a app.UseIdentity en el método Startup.Configure del servicio.
El uso de ASP. NET Core Identity permite varios escenarios:
ASP. NET Core Identity también soporta autenticación de dos factores.
Para los escenarios de autenticación que usan almacenamiento local de datos de usuario y que persisten la identidad entre las peticiones usando cookies (como es típico para las aplicaciones web de MVC), ASP.NET Core Identity es una solución recomendada.
El ASP.NET Core también soporta el uso de proveedores externos de autenticación para permitir que los usuarios inicien sesión a través de los flujos de OAuth 2.0. Esto significa que los usuarios pueden iniciar sesión usando los procesos de autenticación existentes de proveedores como Microsoft, Google, Facebook o Twitter y asociar esas identidades con una identidad ASP. NET Core en su aplicación.
Para utilizar la autenticación externa, incluya el middleware de autenticación apropiado en el pipeline de peticiones HTTP de su aplicación. Este middleware es responsable de gestionar las peticiones de rutas URI de retorno del proveedor de autenticación, capturar la información de identidad y ponerla a disposición a través del método SignInManager.GetExternalLoginInfo.
En la siguiente tabla se muestran los proveedores de autenticación externa más populares y sus paquetes NuGet asociados.
Provider |
Package |
Microsoft |
Microsoft.AspNetCore.Authentication.MicrosoftAccount |
Microsoft.AspNetCore.Authentication.Google |
|
Microsoft.AspNetCore.Authentication.Facebook |
|
Microsoft.AspNetCore.Authentication.Twitter |
En todos los casos, el middleware se registra con una llamada a un método de registro similar a app.use{ExternalProvider}Authentication en Startup.Configure. Estos métodos de registro toman un objeto de opciones que contiene un ID de aplicación e información secreta (una contraseña, por ejemplo), según lo necesite el proveedor. Los proveedores de autenticación externa requieren que la aplicación esté registrada (como se explica en la documentación de ASP.NET Core) para que puedan informar al usuario qué aplicación está solicitando acceso a su identidad.
Una vez que el middleware está registrado en Startup.Configure, puede solicitar a los usuarios que inicien sesión desde cualquier acción del controlador. Para ello, cree un objeto AuthenticationProperties que incluya el nombre del proveedor de autenticación y una URL de redireccionamiento. A continuación, devuelva una respuesta Challenge que pasa el objeto AuthenticationProperties. El siguiente código muestra un ejemplo de ello.
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return Challenge(properties, provider); |
El parámetro redirectUrl incluye la URL a la que el proveedor externo debería redirigir una vez que el usuario se haya autenticado. La URL debe representar una acción que inicie sesión del usuario basándose en información de identidad externa, como en el siguiente ejemplo simplificado:
// Sign in the user with this external login provider if the user
// already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
return RedirectToLocal(returnUrl);
}
else
{
ApplicationUser newUser = new ApplicationUser
{
// The user object can be constructed with claims from the
// external authentication provider, combined with information
// supplied by the user after they have authenticated with
// the external provider.
UserName = info.Principal.FindFirstValue(ClaimTypes.Name),
Email = info.Principal.FindFirstValue(ClaimTypes.Email)
};
var identityResult = await _userManager.CreateAsync(newUser);
if (identityResult.Succeeded)
{
identityResult = await _userManager.AddLoginAsync(newUser, info);
if (identityResult.Succeeded)
{
await _signInManager.SignInAsync(newUser, isPersistent: false);
}
return RedirectToLocal(returnUrl);
}
}
Si usted elige la opción de autenticación Individual User Account cuando crea el proyecto de aplicación web ASP.NET Core en Visual Studio, se incluye en el proyecto todo el código necesario para iniciar sesión con un proveedor externo, como se muestra en la Figura 9-3.
Figura 9-3. Seleccionando una opción para usar autenticación externa al crear un proyecto de aplicación web
Además de los proveedores de autenticación externa listados anteriormente, hay disponibles paquetes de terceros que proporcionan middleware para utilizar muchos más proveedores de autenticación externa. Puede ver una lista en el repositorio AspNet.Security.OAuth.Providers en GitHub.
Por supuesto, también puede crear su propio middleware de autenticación externa.
La autenticación con ASP.NET Core Identity (incluyendo también a los proveedores externos) funciona bien para muchos escenarios de aplicaciones web, en los que es adecuado almacenar la información del usuario en una cookie. En otros escenarios, sin embargo, las cookies no son un medio natural para persistir y transmitir datos.
Por ejemplo, en una aplicación API Web ASP.NET Core que expone endpoints RESTful a los que pueden acceder las aplicaciones de una sola página (SPA), clientes nativos o incluso otras APIs Web, normalmente desea utilizar la autenticación de token del portador en su lugar. Estos tipos de aplicaciones no funcionan con cookies, pero pueden recuperar fácilmente un token e incluirlo en la cabecera de autorización de peticiones posteriores. Para habilitar la autenticación por token, ASP.NET Core soporta varias opciones para usar OAuth 2.0 y OpenID Connect.
Si la información del usuario se almacena en Azure Active Directory u otra solución de identidad que soporte OpenID Connect u OAuth 2.0, puede utilizar el paquete Microsoft.AspNetCore.Authentication.OpenIdConnect para autenticarse usando el flujo de trabajo de OpenID Connect. Por ejemplo, una aplicación web ASP.NET Core puede usar middleware de ese paquete para autenticarse contra Azure Active Directory, como se muestra en el siguiente ejemplo:
// Configure the OWIN pipeline to use OpenID Connect auth app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions { ClientId = Configuration["AzureAD:ClientId"], Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAd:Tenant"]), ResponseType = OpenIdConnectResponseType.IdToken, PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"] }); |
Los valores de Configuration[“… son los que se crean al registrar su aplicación en Azure Active Directory. Un ID único de cliente puede ser compartido entre múltiples microservicios en una aplicación si todos ellos necesitan autenticar usuarios a través de Azure Active Directory.
Tenga en cuenta que cuando utiliza este flujo de trabajo, no se necesita el middleware de ASP.NET Core Identity, ya que toda la información de almacenamiento de usuario y autenticación es manejada por Azure Active Directory.
Si prefiere emitir tokens de seguridad para usuarios locales de ASP.NET Core Identity, en lugar de usar un proveedor de identidad externo, puede aprovechar algunas librerías de terceros.
IdentityServer4 y OpenIddict son proveedores de OpenID Connect que se integran fácilmente con ASP.NET Core Identity para poder emitir tokens de seguridad desde un servicio ASP.NET Core. La documentación de IdentityServer4 contiene instrucciones detalladas para el uso de la librería. Sin embargo, los pasos básicos para usar IdentityServer4 para emitir tokens son los siguientes.
Cuando especifique los clientes y recursos que utilizará IdentityServer4, puede pasar una colección IEnumerable<T> del tipo apropiado a los métodos que reciben clientes o almacenes de recursos en memoria. Para escenarios más complejos, puede proporcionar tipos de proveedores de clientes o de recursos mediante la inyección de dependencias.
Un ejemplo de configuración para que IdentityServer4 use recursos y clientes en memoria, proporcionados por un tipo de IClientStore personalizado, se podría parecer lo siguiente:
// Add IdentityServer services services.AddSingleton<IClientStore, CustomClientStore>();
services.AddIdentityServer() .AddSigningCredential("CN=sts") .AddInMemoryApiResources(MyApiResourceProvider.GetAllResources()) .AddAspNetIdentity<ApplicationUser>(); |
Autenticar contra un endpoint OpenID Connect o la emisión de sus propios tokens de seguridad cubre algunos escenarios. Pero ¿qué pasa con un servicio que simplemente necesita limitar el acceso a aquellos usuarios que tienen tokens de seguridad válidos que fueron proporcionados por un servicio diferente?
Para ese escenario, el middleware de autenticación que maneja los tokens JWT está disponible en el paquete Microsoft.AspNetCore.Authentication.JwtBearer. JWT significa "JSON Web Token" y es un formato común de token de seguridad (definido por RFC 7519) para la comunicación de afirmaciones (claims) de seguridad. Un simple ejemplo de cómo usar middleware para consumir tales tokens podría ser algo como lo siguiente. Este código debe preceder las llamadas al middleware ASP. NET Core MVC (app.UseMvc).
app.UseJwtBearerAuthentication(new JwtBearerOptions() { Audience = "http://localhost:5001/", Authority = "http://localhost:5000/", AutomaticAuthenticate = true }); |
Los parámetros en esta forma de usarlo son:
En este ejemplo no se utiliza otro parámetro, RequireHttpsMetadata. Es útil para fines de prueba; este parámetro se configura como false para que pueda probar en entornos donde no tenga certificados. En los despliegues del mundo real, los tokens JWT siempre se deben pasar sólo por HTTPS.
Con este middleware instalado, los tokens JWT se extraen automáticamente de las cabeceras de autorización. A continuación, se deserializa, se valida (utilizando los valores de los parámetros de Audience y Authority) y se almacena como información de usuario, a la que posteriormente se puede hacer referencia mediante acciones MVC o filtros de autorización.
El middleware de autenticación por token JWT también puede soportar escenarios más avanzados, como el uso de un certificado local para validar un token si la autoridad no está disponible. Para este escenario, puede especificar un objeto TokenValidationParameters en el objeto JwtBearerOptions.
Después de la autenticación, las APIs Web de ASP.NET Core necesitan autorizar el acceso. Este proceso permite que un servicio ponga APIs a disposición de algunos usuarios autenticados, pero no de todos. La autorización se puede hacer según las funciones de los usuarios o en base a una política personalizada, que puede incluir la inspección de las claims (afirmaciones) u otras técnicas.
Restringir el acceso a una ruta ASP.NET Core MVC es tan fácil como aplicar un atributo Authorize al método de la acción (o a la clase del controlador si todas las acciones requieren autorización), como se muestra en el siguiente ejemplo:
public class AccountController : Controller { public ActionResult Login() { }
[Authorize] public ActionResult Logout() { } |
Por defecto, al usar el atributo Authorize sin parámetros, se limitará el acceso a los usuarios autenticados para ese controlador o acción. Para restringir aún más la disponibilidad de una API, sólo para usuarios específicos, el atributo puede ampliarse para especificar roles o políticas que los usuarios deben satisfacer.
ASP.NET Core Identity incluye el concepto de roles. Además de los usuarios, ASP.NET Core Identity almacena información sobre los diferentes roles usados por la aplicación y los usuarios a quienes se les asignan. Estas asignaciones se pueden modificar programáticamente con el tipo RoleManager (que actualiza las funciones en el almacenamiento persistente) y el tipo de UserManager (que gestiona la asignación de roles a usuarios).
Si está autenticando con tokens JWT, el middleware JWT ASP.NET Core cargará los roles del usuario basándose en las claims de roles encontradas en el token. Para limitar el acceso a una acción o controlador MVC a usuarios con roles específicos, puede incluir un parámetro Roles en la anotación (atributo) Authorize, como se muestra en el ejemplo siguiente:
[Authorize(Roles = "Administrator, PowerUser")] public class ControlPanelController : Controller { public ActionResult SetTime() { }
[Authorize(Roles = "Administrator")] public ActionResult ShutDown() { } } |
En este ejemplo, sólo los usuarios con los roles Administrator o PowerUser pueden acceder a las API en el controlador ControlPanel (como ejecutar la acción SetTime). La API ShutDown se restringe aún más, para permitir el acceso sólo a los usuarios en el rol Administrator.
Para exigir que un usuario tenga múltiples roles, utilice varios atributos de Authorize, como se muestra en el ejemplo siguiente:
[Authorize(Roles = "Administrator, PowerUser")] [Authorize(Roles = "RemoteEmployee ")] [Authorize(Policy = "CustomPolicy")] public ActionResult API1 () { } |
En este ejemplo, para llamar a API1, un usuario debe:
Las reglas de autorización personalizadas también se pueden escribir usando políticas de autorización. En esta sección le ofrecemos una visión general. Más detalles están disponibles en el Taller de Autorización ASP.NET en línea.
Las políticas de autorización personalizadas se registran en el método Startup.ConfigureServices usando el método services.AddAuthorization. Este método recibe un delegado que configura un parámetro AuthorizationOptions.
services.AddAuthorization(options => { options.AddPolicy("AdministratorsOnly", policy => policy.RequireRole("Administrator")); options.AddPolicy("EmployeesOnly", policy => policy.RequireClaim("EmployeeNumber")); options.AddPolicy("Over21", policy => policy.Requirements.Add(new MinimumAgeRequirement(21))); }); |
Como se muestra en el ejemplo, las políticas pueden asociarse con diferentes tipos de necesidades. Después de que las políticas se registran, se pueden aplicar a una acción o controlador al pasar el nombre de la política como el parámetro Policy del atributo Authorize (por ejemplo, [Authorize(Policy=”EmployeesOnly”)]. Las políticas pueden tener múltiples requisitos, no sólo uno, como se muestra en estos ejemplos.
En el ejemplo anterior, la primera llamada de AddPolicy es sólo una forma alternativa de autorizar por rol. Si se aplica [Authorize(Policy="AdministratorsOnly")] se aplica a una API, sólo los usuarios con el rol Administrator podrán acceder a ella.
La segunda llamada AddPolicy demuestra una manera fácil de exigir que el usuario debe tener una claim particular. El método de RequireClaim también recibe, opcionalmente, los valores esperados para la claim. Si se especifican valores, el requisito sólo se cumple si el usuario tiene una claim del tipo correcto y uno de los valores especificados. Si está utilizando el middleware de autenticación por token JWT, todas las propiedades del JWT estarán disponibles como claims de usuario.
La política más interesante que se muestra aquí, es el tercer método de AddPolicy, ya que utiliza un requisito personalizado de autorización. Al usar requisitos personalizados de autorización, puede tener un gran control sobre cómo se realiza la autorización. Para que esto funcione, debe implementar estos tipos:
Si el usuario cumple con los requisitos, una llamada a context.Succeed indica que el usuario está autorizado. Si hay varias maneras en que un usuario puede satisfacer un requisito de autorización, se pueden crear varios manejadores.
Además de registrar los requisitos de las políticas personalizadas con las llamadas a AddPolicy, también necesita registrar los manejadores de requisitos personalizados mediante Inyección de Dependencias (servicios.AddTransient<IAuthorizationHandler, MinimumAgeHandler>()).
Un ejemplo de un requisito personalizado de autorización y su manejador para verificar la edad del usuario (basado en una claim DateOfBirth) está disponible en la documentación de autorización de ASP.NET Core.
Para conectarse con recursos protegidos y otros servicios, las aplicaciones ASP.NET Core típicamente necesitan usar cadenas de conexión, contraseñas u otras credenciales que contengan información confidencial. Estas partes de información sensible se llaman secretos. Es una buena práctica no incluir secretos en el código fuente y ciertamente no almacenar secretos en el sistema de control de versiones. En su lugar, debería utilizar el modelo de configuración de ASP.NET Core para leer los secretos desde ubicaciones más seguras.
Debe separar los secretos para acceder a los recursos de desarrollo y pre-producción (staging) de los que se usan para acceder a los recursos de producción, porque diferentes individuos necesitarán acceder a esos conjuntos diferentes de secretos. Para almacenar secretos usados durante el desarrollo, los enfoques comunes son almacenar secretos en variables de entorno o mediante la herramienta ASP.NET Core Secret Manager. Para un almacenamiento más seguro en entornos de producción, los microservicios pueden almacenar secretos en una Azure Key Vault.
Una forma de mantener secretos fuera del código fuente es que los desarrolladores establezcan secretos basados en cadenas, como variables de entorno en sus máquinas de desarrollo. Cuando utilice variables de entorno para almacenar secretos con nombres jerárquicos (los anidados en secciones de configuración), debe crear un nombre para las variables de entorno que incluya la jerarquía completa del nombre del secreto, delimitada por dos puntos (:).
Por ejemplo, establecer una variable de entorno Logging:LogLevel:Default a Debug es equivalente al valor de configuración del siguiente fichero JSON:
{ "Logging": { "LogLevel": { "Default": "Debug" } } } |
Para acceder a estos valores desde variables de entorno, la aplicación sólo necesita llamar a AddEnvironmentVariables en su ConfigurationBuilder al construir un objeto IConfigurationRoot.
Tenga en cuenta que las variables de entorno generalmente se almacenan como texto plano, por lo que, si la máquina o el proceso con las variables de entorno se ve comprometido, los valores de las variables de entorno serán visibles.
La herramienta ASP.NET Core Secret Manager proporciona otro método para mantener los secretos fuera del código fuente. Para usar el Secret Manager Tool, incluya una referencia de herramientas (DotNetCliToolReference) al paquete Microsoft.Extensions.SecretManager.Tools en su fichero de proyecto. Una vez que esa dependencia está presente y ha sido restaurada, se puede usar el comando dotnet user-secrets para establecer el valor de los secretos desde la línea de comandos. Estos secretos se almacenarán en un fichero JSON en el directorio de perfiles del usuario (los detalles varían según el sistema operativo), lejos del código fuente.
Los secretos establecidos por la Secret Manager tool están organizados por la propiedad UserSecretsId del proyecto que está utilizando los secretos. Por lo tanto, debe asegurarse de establecer la propiedad UserSecretsId en su fichero de proyecto (como se muestra en el fragmento a continuación). La cadena real utilizada como ID no es importante mientras sea única en el proyecto.
<PropertyGroup> <UserSecretsId>UniqueIdentifyingString</UserSecretsId> </PropertyGroup> |
El uso de secretos almacenados con la Secret Manager tool en una aplicación, se logra llamando a AddUserSecrets<T> en la instancia ConfigurationBuilder para incluir secretos para la aplicación en su configuración. El parámetro genérico T debe ser un tipo del ensamblado al que se aplicó el UserSecretId. Normalmente, el uso de AddUserSecrets<Startup> está bien.
Los secretos almacenados como variables de entorno o almacenados por la Secret Manager tool siguen almacenados localmente y sin encriptar en la máquina. Una opción más segura para guardar secretos es el Azure Key Vault, que proporciona una ubicación central y segura para guardar llaves y secretos.
El paquete Microsoft.Extensions.Configuration.AzureKeyVault permite que una aplicación ASP.NET Core pueda leer la información de configuración del Azure Key Vault. Para empezar a usar secretos de Azure Key Vault, siga estos pasos:
Alternativamente, si desea que su aplicación se autentique utilizando un certificado en lugar de una contraseña o un secreto de cliente, puede utilizar el nuevo cmdlet PowerShell New-AzureRmADApplication. El certificado que registre con Azure Key Vault sólo necesita su clave pública. (Su aplicación utilizará la clave privada.)
$sp = New-AzureRmADServicePrincipal -ApplicationId "<Application ID guid>"
Set-AzureRmKeyVaultAccessPolicy -VaultName "<VaultName>" -ServicePrincipalName $sp.ServicePrincipalNames[0] -PermissionsToSecrets all -ResourceGroupName "<KeyVault Resource Group>"
Desde la versión 3.14.0 del paquete Microsoft.IdentityModel.Clients.ActiveDirectory está disponible el método AddAzureKeyVault para.NET Standard, .NET Core y .NET Framework. Las aplicaciones ASP.NET Core también pueden acceder a un Azure Key Vault con autenticación basada en certificados mediante la creación explícita de un objeto KeyVaultClient, como se muestra en el siguiente ejemplo:
// Configure Key Vault client
var kvClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(async
(authority, resource, scope) =>
{
var cert = // Get certificate from local store/file/key vault etc. as needed
// From the Microsoft.IdentityModel.Clients.ActiveDirectory pacakge
var authContext = new AuthenticationContext(authority,
TokenCache.DefaultShared);
var result = await authContext.AcquireTokenAsync(resource,
// From the Microsoft.Rest.ClientRuntime.Azure.Authentication pacakge
new ClientAssertionCertificate("{Application ID}", cert));
return result.AccessToken;
}));
// Get configuration values from Key Vault
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
// Other configuration providers go here.
.AddAzureKeyVault("{KeyValueUri}", kvClient,
new DefaultKeyVaultSecretManager());
En este ejemplo, la llamada a AddAzureKeyVault viene al final del registro del proveedor de configuración. Es una buena práctica registrar Azure Key Vault como el último proveedor de configuración para que tenga la oportunidad de invalidar los valores de configuración de proveedores anteriores y para que ningún valor de configuración de otras fuentes reemplace los de la bóveda de claves.