Configuración de roles y claims personalizados en IdentityServer4

Al implementar seguridad en APIs de ASP.NET Core utilizando IdentityServer4, uno de los desafíos más comunes es lograr que el atribuot [Authorize] basado en roles funcione correctamente. A menudo, aunque la autenticación sea exitosa, el servidor responde con un error 401 Unauthorized debido a la ausencia de claims específicos en el token de acceso.

El problema: El atirbuto Authorize y la ausencia de roles

Consideremos un controlador protegido donde restringimos el acceso solo a usuarios con el rol "Administrador":

[Route("api/[controller]")]
[Authorize(Roles = "Administrador")]
[ApiController]
public class PerfilUsuarioController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> ObtenerDetalles()
    {
        var servicio = new ProveedorInformacion();
        var datos = await servicio.ConsultarAsync();
        return Ok(datos);
    }
}

A pesar de que el cliente se autentica correctamente, el middleware de autorización falla. Al inspeccionar el contenido del JWT (JSON Web Token) devuelto por IdentityServer4, observamos que el claim de tipo role no está presente, lo que impide que ASP.NET Core valide la pertenencia al grupo requerido:

{
    "nbf": 1587301921,
    "exp": 1587305521,
    "iss": "http://localhost:5000",
    "aud": "api_recurso_gestion",
    "client_id": "cliente_web_app",
    "sub": "c6c18d4d-c28e-4de5-86dd-779121216204",
    "scope": [
        "api_recurso_gestion",
        "offline_access"
    ]
}

Configuración de recursos de identidad y API

Para solucionar esto, es necesario configurar correctamente el archivo Config.cs en el servidor de identidad. Muchos desarrolladores intentan añadir el claim de rol en los IdentityResources, pero esto suele afectar principalmente al ID Token y no necesariamente al Access Token que consume la API.

Primero, definimos los recursos de identidad:

public static IEnumerable<IdentityResource> RecursosIdentidad =>
    new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResource("roles", "Roles del usuario", new[] { JwtClaimTypes.Role })
    };

Y aseguramos que el usuario en la base de datos tenga asignado el claim correspondiente:

var usuarioNuevo = new UsuarioAplicacion { UserName = "admin_user" };
var resultado = await gestorUsuarios.CreateAsync(usuarioNuevo, "PasswordSecuro123!");

if (resultado.Succeeded)
{
    await gestorUsuarios.AddClaimsAsync(usuarioNuevo, new Claim[]
    {
        new Claim(JwtClaimTypes.Role, "Administrador"),
        new Claim(JwtClaimTypes.Name, "Admin Principal")
    });
}

La solución definitiva: ApiResource con UserClaims

La forma más efectiva de incluir claims del usuario dentro del token de acceso destinado a una API específica es mediante la propiedad UserClaims en la definición del ApiResource. Esto le indica a IdentityServer4 que, cuando un cliente solicite acceso a ese recurso, debe extraer ciertos claims del perfil del usuario e incluirlos en el JWT.

public static IEnumerable<ApiResource> RecursosApi =>
    new List<ApiResource>
    {
        new ApiResource("api_recurso_gestion", "Mi API de Gestión")
        {
            // Aquí se especifican los claims que deben incluirse en el token de acceso
            UserClaims = { JwtClaimTypes.Role }
        }
    };

public static IEnumerable<Client> Clientes =>
    new List<Client>
    {
        new Client
        {
            ClientId = "cliente_web_app",
            ClientSecrets = { new Secret("clave_secreta".Sha256()) },
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
            AllowedScopes = { "api_recurso_gestion", "roles" },
            AllowOfflineAccess = true
        }
    };

Con esta configuración, el token generado ahora incluirá el campo role:

{
    "iss": "http://localhost:5000",
    "aud": "api_recurso_gestion",
    "client_id": "cliente_web_app",
    "sub": "c6c18d4d-c28e-4de5-86dd-779121216204",
    "role": "Administrador",
    "scope": [
        "api_recurso_gestion",
        "roles"
    ]
}

Es importante notar la diferencia entre los claims del cliente y los claims del usuario. Si se añaden claims directamente en el objeto Client mediante la propiedad Claims, estos se identificarán normalmente como client_role y no servirán para la autorización basada en roles de usuario estándar, ya que representan la identidad de la aplicación y no la del individuo autenticado.

Etiquetas: IdentityServer4 ASP.NET Core JWT OAuth2 authorization

Publicado el 7-24 22:23