Las ventajas que te ofrece Microsoft Azure y el mundo.NET

Cómo securizar tus apps con Identity Server y .NET Core (Parte III)

Continuamos con la serie de artículos sobre Identity Server 🙂 Tras » Cómo securizar tus apps con Identity Server y .NET Core (Parte I)», y «Cómo securizar tus apps con Identity Server y .NET Core (Parte II)», veremos segmentar nuestra API de una forma similar a Microsoft Graph. Para ello vamos a aplicar un concepto muy chulo que provee ASP .Net Core:  Autorización basada en claims (existe otro tipo de autorización basado en directivas o requisitos que puedes ver en Autorización basada en directivas en ASP.NET Core).

Y ahora, a segmentar nuestra API

Si recordáis, se han definido dos ámbitos (APIEmployee y APICustomer) en la configuración del cliente.

new Client
{
	ClientId = "openIdEncamina",
	ClientName = "Example Implicit Client Application",
	ClientSecrets = new List { new Secret("superSecretPassword".Sha256()) },
	AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
	AlwaysSendClientClaims=true,
	AllowedScopes = new List
	{
		IdentityServerConstants.StandardScopes.OpenId,
		IdentityServerConstants.StandardScopes.Profile,
		IdentityServerConstants.StandardScopes.Email,
		"APIEmployee", "APICustomer",
		},
	RedirectUris = new List { "https://localhost:44392/signin-oidc" },
	PostLogoutRedirectUris = new List { "https://localhost:44392/" }
}

Y en la configuración de OpenId de la aplicación Web sólo se permite usar el scope de APIEmployee.

AddOpenIdConnect("oidc", options =>
{
     options.Authority = "https://localhost:44384/";
     options.ClientId = "openIdEncamina";
     options.ClientSecret = "superSecretPassword";
     options.SignInScheme = "cookie";
     options.SaveTokens = true;
     options.ResponseType = "code id_token";
     options.GetClaimsFromUserInfoEndpoint = true;

     options.Scope.Add("openid");
     options.Scope.Add("profile");
     options.Scope.Add("APIEmployee");                 
});

Teniendo esto en cuenta, vamos a implementar lo que se denominan políticas en nuestra API .NET Core. Para ello, lo primero que se debe hacer es instalar el paquete Nuget IdentityServer4.AccessTokenValidation y dentro de nuestro proyecto añadiremos el siguiente código en el Startup.cs, con el que implementaremos un método para establecer las políticas.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvcCore()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
            .AddAuthorization()
            .AddJsonFormatters();

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            
    services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(options =>
            {
                options.RequireHttpsMetadata = false;
                options.Authority = "https://localhost:44384/";
                options.ApiName = "EncaminaAPI";
            });

            services.AddPolicies(); // establecemos políticas
        }
public static class Policy
{
   public static void AddPolicies(this IServiceCollection services)
   {
       services.AddAuthorization(options =>
                    options.AddPolicy("Employee",
                                 policy => policy.RequireClaim("scope", "APIEmployee")));

            services.AddAuthorization(options =>
                   options.AddPolicy("Customer",
                                policy => policy.RequireClaim("scope", "APICustomer")));
    }
}

Lo que estamos estableciendo es que se creen dos políticas en las cuales se revise un claim denominado scope y cuyo contenido sea APIEmployee o APICustomer.
Esto es una de las cosas más chulas de ASP.NET Core, ya que permite segmentar nuestra API (controladores) para que dado un token se valide o no teniendo en cuenta en la configuración del cliente.

Para implementar esto, en nuestros controladores, agregamos el atributo Authorize con unos parámetros adicionales que son el nombre de la política y el tipo de autenticación, en este caso quedarían así:

[Route("api/[controller]")]
[Authorize("Employee", AuthenticationSchemes = "Bearer")]
[ApiController]
public class EmployeeController : ControllerBase
{

      [HttpGet]
      public ActionResult<IEnumerable<Employee>> Get()
      {
            return new Employee[]
            {
                new Employee
                {
                    Name="Cristiano Ronaldo"
                },
                new Employee
                {
                    Name="Luka Modrid"
                },
                new Employee
                {
                    Name="Mohamed Salah"
                },
            };
      }
}

[Route("api/[controller]")]
[Authorize("Customer", AuthenticationSchemes = "Bearer")]
[ApiController]
public class CustomerController : ControllerBase
{
        [HttpGet]
        public ActionResult<IEnumerable<Customer>> Get()
        {
            return new Customer[]
            {
                new Customer
                {
                    Name="Real Madrid"
                },
                new Customer
                {
                    Name="Milan"
                },
                new Customer
                {
                    Name="Atletico de Madrid"
                },
                new Customer
                {
                    Name="F.C. Barcelona"
                }
            };
        }
    }

Vale, ¿y esto funciona? Veamos una prueba vía Postman, empleando para ello el token obtenido en la autenticación de nuestra aplicación web.
Consumiendo el API de Empleados que sí tenemos permisos y agregando una cabecera Authorization con contenido Bearer validamos que tenemos respuesta de nuestra API.

Y consumiendo el de clientes y usando el mismo token, se valida que NO estamos autorizados. Obtenemos un error Http 403.

Esto funciona chachi!!!

¿Y si se pudiera insertar claims personalizados en el token de acceso usando Identity Server?

Como regalito os voy a contar cómo desde Identity Server, que hemos visto que cuando hacemos login se genera un token con unos claims por defecto (como nuestros scopes permitidos, el identificador del usuario etc…), podemos establecer los claims que queramos.

Imaginad que estamos implementando una aplicación bancaria y necesitamos que en nuestro token de acceso estén todos los números de cuenta de un cliente (para luego poder validar si una petición a la API es válida sobre las cuentas de un cliente o NO es válida si es sobre otras cuentas).

Para ello se crea una clase que implementa el interfaz IdentityServer4.Services.IProfileService.

/// <summary>
/// Identity Server Profile service 
/// </summary>
public class MyProfileService : IProfileService
{
    private readonly ApplicationUserManager _userManager; 

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="userManager"></param>
    public ProfileService(ApplicationUserManager userManager)
    {
        _userManager = userManager; 
    }

    /// <summary>
    /// Get profile data
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var user = await _userManager.GetUserAsync(context.Subject);
        var claims = await CustomerAccountsToClaimsAsync(user.UserId).ConfigureAwait(false);

        // set accounts to claims in access_token
        context.IssuedClaims.AddRange(claims);
    }

    /// <summary>
    /// Is user active
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task IsActiveAsync(IsActiveContext context)
    {
        var user = await _userManager.GetUserAsync(context.Subject).ConfigureAwait(false);
        context.IsActive = (user != null) &amp;&amp; user.LockoutEnabled;
    }

    private async Task<List<Claim>> CustomerAccountsToClaimsAsync(int customerId)
    {
        const string claimType = "accounts";

        var result = <llamar a nuestra API para recuperar las cuentas de un cliente>;
        var claims = new List<Claim>();

        result?.ForEach(account =>
        {
            claims.Add(new Claim(claimType, account.Account?.Id.ToString()));
        });

        return claims;
    }
}

Luego, en Startup.cs de nuestro Identity Server establecemos que el servicio de Profile de Identity Server sea el que acabamos de implementar

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients())
             .AddInMemoryIdentityResources(Config.GetIdentityResources()) // damos de alta nuevos recursos de identidad
             .AddTestUsers(Config.GetUsers()); // damos de alta usuarios a autenticar
             .AddProfileService<MyProfileService>(); // usar nuestro servicio de profile personalizado
}

Es fácil ¿verdad?

Conclusiones

En este artículo hemos visto que podemos segmentar nuestra API en scopes o ámbitos, y poder realizar la validación de la autorización empleando para ello políticas basadas en claims. También hemos aprendido a que Identity Server, a la hora de devolver el token de acceso, establezca claims personalizados.

¡HAPPY CODING!

mm

Sobre Sergio Parra Guerra

Sergio Parra es Ingeniero Técnico en Informática de Sistemas por la UPSAM. Tiene a sus espaldas muchísimas certificaciones entre las cuales Microsoft Certified Professional y ex Microsoft MVP Visual Studio and Development Technologies. Actualmente es un magnífico Software & Cloud Architect en ENCAMINA.
Esta entrada ha sido publicada en .NET. Enlace permanente.
Suscríbete a Piensa en Sofware desarrolla en Colores

Suscríbete a Piensa en Sofware desarrolla en Colores

Recibe todas las actualizaciones semanalmente de nuestro blog

You have Successfully Subscribed!

ENCAMINA, piensa en colores