¿Cómo se crea un AuthorizeAttribute personalizado en ASP.NET Core?

428

Estoy tratando de hacer un atributo de autorización personalizado en ASP.NET Core. En versiones anteriores era posible anular bool AuthorizeCore(HttpContextBase httpContext). Pero esto ya no existe en AuthorizeAttribute.

¿Cuál es el enfoque actual para hacer un AuthorizeAttribute personalizado?

Lo que estoy tratando de lograr: estoy recibiendo una ID de sesión en la Autorización de encabezado. A partir de ese ID sabré si una acción en particular es válida.

jltrem
fuente
No estoy seguro de cómo hacerlo, pero MVC es de código abierto. Puede extraer el repositorio de github y buscar implementaciones de IAuthorizationFilter. Si tengo tiempo hoy, te buscaré y publicaré una respuesta real, pero no prometo nada. repositorio de github: github.com/aspnet/Mvc
bopapa_1979
OK, fuera de tiempo, pero busque usos de AuthorizationPolicy en el MVC Repo, que usa AuthorizeAttribute, en el aspnet / Security repo, aquí: github.com/aspnet/Security . Alternativamente, busque en el repositorio de MVC el espacio de nombres donde parece que residen las cosas de seguridad que le interesan, que es Microsoft.AspNet.Authorization. Lo siento, no puedo ser más útil. ¡Buena suerte!
bopapa_1979

Respuestas:

446

El enfoque recomendado por el equipo de ASP.Net Core es utilizar el nuevo diseño de política que está completamente documentado aquí . La idea básica detrás del nuevo enfoque es usar el nuevo atributo [Autorizar] para designar una "política" (por ejemplo, [Authorize( Policy = "YouNeedToBe18ToDoThis")]cuando la política se registra en Startup.cs de la aplicación para ejecutar algún bloque de código (es decir, asegurarse de que el usuario tenga un reclamo de antigüedad) donde la edad es 18 años o más).

El diseño de la política es una gran adición al marco y el equipo de ASP.Net Security Core debe ser elogiado por su introducción. Dicho esto, no es adecuado para todos los casos. La desventaja de este enfoque es que no proporciona una solución conveniente para la necesidad más común de simplemente afirmar que un controlador o acción determinada requiere un tipo de reclamo dado. En el caso de que una aplicación pueda tener cientos de permisos discretos que rigen las operaciones CRUD en recursos REST individuales ("CanCreateOrder", "CanReadOrder", "CanUpdateOrder", "CanDeleteOrder", etc.), el nuevo enfoque requiere repetitivo de uno a otro una asignación entre un nombre de póliza y un nombre de reclamo (p. ej.options.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));), o escribir algún código para realizar estos registros en tiempo de ejecución (por ejemplo, leer todos los tipos de reclamo de una base de datos y realizar la llamada mencionada en un bucle). El problema con este enfoque para la mayoría de los casos es que es una sobrecarga innecesaria.

Si bien el equipo de ASP.Net Core Security recomienda nunca crear su propia solución, en algunos casos esta puede ser la opción más prudente para comenzar.

La siguiente es una implementación que utiliza IAuthorizationFilter para proporcionar una manera simple de expresar un requisito de reclamo para un controlador o acción determinada:

public class ClaimRequirementAttribute : TypeFilterAttribute
{
    public ClaimRequirementAttribute(string claimType, string claimValue) : base(typeof(ClaimRequirementFilter))
    {
        Arguments = new object[] {new Claim(claimType, claimValue) };
    }
}

public class ClaimRequirementFilter : IAuthorizationFilter
{
    readonly Claim _claim;

    public ClaimRequirementFilter(Claim claim)
    {
        _claim = claim;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
        if (!hasClaim)
        {
            context.Result = new ForbidResult();
        }
    }
}


[Route("api/resource")]
public class MyController : Controller
{
    [ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")]
    [HttpGet]
    public IActionResult GetResource()
    {
        return Ok();
    }
}
Derek Greer
fuente
80
Esto debe marcarse como la RESPUESTA CORRECTA. Aquí puede ver cómo la gente de Microsoft considera los comentarios de los desarrolladores. No entiendo la razón por la que son tan "de mente cerrada" por esto, ya que es una situación muy común tener una miríada de permisos diferentes, tener que codificar una política para cada uno es una exageración total. Estuve buscando esto por tanto tiempo ... (ya hice esta pregunta hace casi dos años, cuando vNext todavía era una apuesta aquí: stackoverflow.com/questions/32181400/… pero todavía estamos atrapados allí)
Vi100
3
Esto es bueno Tenemos middleware de autenticación en la API web pero seguridad específica en los permisos de autorización por rol; así que tener que agregar un atributo como: [MyAuthorize (MyClaimTypes.Permission, MyClaimValueTypes.Write, MyPermission.Employee)] se ve muy bien.
Mariano Peinador
44
@Derek Greer: Esta es la mejor respuesta. Sin embargo, está implementando un ActionFilter que se ejecuta después de Autorizar filtro de acción. ¿Hay alguna forma de implementar y autorizar el filtro de acción?
Jacob Phan
66
@JacobPhan Tienes razón, esto se implementaría mejor usando la interfaz IAuthorizationFilter. He actualizado el código para reflejar los cambios.
Derek Greer
3
entonces new ForbidResult()no funciona (causa una excepción / 500) porque no tiene un esquema de autorización asociado. ¿Qué usaría para este caso?
Sinaesthetic
252

Soy la persona de seguridad de asp.net. En primer lugar, permítanme disculparme porque nada de esto está documentado aún fuera de la muestra de la tienda de música o de las pruebas unitarias, y todo se está refinando en términos de API expuestas. La documentación detallada está aquí .

No queremos que escriba atributos de autorización personalizados. Si necesita hacer eso, hemos hecho algo mal. En cambio, debe escribir los requisitos de autorización .

La autorización actúa sobre las identidades. Las identidades se crean por autenticación.

Usted dice en los comentarios que desea verificar una ID de sesión en un encabezado. Su ID de sesión sería la base de la identidad. Si quisiera usar el Authorizeatributo, escribiría un middleware de autenticación para tomar ese encabezado y convertirlo en un autenticado ClaimsPrincipal. Luego verificaría eso dentro de un requisito de autorización. Los requisitos de autorización pueden ser tan complicados como desee, por ejemplo, aquí hay uno que toma una fecha de nacimiento de la identidad actual y autorizará si el usuario es mayor de 18 años;

public class Over18Requirement : AuthorizationHandler<Over18Requirement>, IAuthorizationRequirement
{
        public override void Handle(AuthorizationHandlerContext context, Over18Requirement requirement)
        {
            if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
            {
                context.Fail();
                return;
            }

            var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
            int age = DateTime.Today.Year - dateOfBirth.Year;
            if (dateOfBirth > DateTime.Today.AddYears(-age))
            {
                age--;
            }

            if (age >= 18)
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
        }
    }
}

Luego, en su ConfigureServices()función, lo conectaría

services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", 
        policy => policy.Requirements.Add(new Authorization.Over18Requirement()));
});

Y finalmente, aplíquelo a un controlador o método de acción con

[Authorize(Policy = "Over18")]
soplar el dardo
fuente
84
Me pregunto ... ¿cómo se implementaría un control de acceso de grano fino con eso? Digamos el ManageStorerequisito de la muestra de Music Store. Como está en la muestra, solo hay una forma de "permitir todo o nada" para hacerlo. ¿Entonces tenemos que crear una nueva política para cada permutación posible? es decir, "Usuarios / Leer", "Usuarios / Crear", "Usuarios / Asignar rol", "Usuarios / Eliminar" si queremos reclamos específicos. ¿Suena como un trabajo de configuración para que funcione y una gran cantidad de políticas solo para administrar reclamos en lugar de un [ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]atributo?
Tseng
84
Debo comentar que todo esto es más complejo que implementar un método de autorización personalizado. Sé cómo quiero que se haga la autorización, podría ir y escribirla en MVC 5, en MVC 6 agregan mucho código "hecho" que en realidad es más complejo de entender que implementar la "cosa" central en sí. Me pone sentado frente a una página tratando de resolver algo en lugar de escribir el código directamente, lo que también es un gran dolor para las personas que usan RDBMS que no sean Microsoft (o No-Sql).
Felype
17
Desde mi punto de vista, esto no resuelve todos los escenarios. Antes de MVC 6, utilicé un atributo Autorizar personalizado para implementar mi propio "Sistema de permisos". Podría agregar el atributo Autorizar a todas las acciones y pasar un permiso específico necesario (como Enum-Value). El permiso en sí se asignó a grupos / usuarios dentro de la base de datos. Entonces, ¿no veo una manera de manejar esto con políticas?
Gerwald
43
Yo, como muchos otros en estos comentarios, estoy muy decepcionado de que el uso de atributos para la autorización se haya neutralizado en gran medida sobre lo que era posible en Web API 2. Lo siento, muchachos, pero su abstracción "requisito" no cubre ningún caso en el que pudiéramos usar previamente parámetros del constructor de atributos para informar un algoritmo de autorización subyacente. Solía ​​ser muy simple hacer algo así [CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]. Podría usar un único atributo personalizado en infinitas formas simplemente modificando los parámetros del constructor.
NathanAldenSr
61
También estoy sorprendido de que el autoproclamado "Líder de seguridad ASP.NET principal" en realidad sugiera utilizar cadenas mágicas (pirateando el significado de IAuthorizeData.Policy) y proveedores de políticas personalizadas para superar esta evidente supervisión, en lugar de abordarla dentro del marco. ¿Pensé que no debíamos crear nuestras propias implementaciones? No nos ha dejado a varios más remedio que volver a implementar la autorización desde cero (nuevamente), y esta vez sin siquiera el beneficio del antiguo Authorizeatributo de la API web . Ahora tenemos que hacerlo en el filtro de acción o en el nivel de middleware.
NathanAldenSr
104

Parece que con ASP.NET Core 2, puede volver a heredar AuthorizeAttribute, solo necesita implementar IAuthorizationFilter(o IAsyncAuthorizationFilter):

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private readonly string _someFilterParameter;

    public CustomAuthorizeAttribute(string someFilterParameter)
    {
        _someFilterParameter = someFilterParameter;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            // it isn't needed to set unauthorized result 
            // as the base class already requires the user to be authenticated
            // this also makes redirect to a login page work properly
            // context.Result = new UnauthorizedResult();
            return;
        }

        // you can also use registered services
        var someService = context.HttpContext.RequestServices.GetService<ISomeService>();

        var isAuthorized = someService.IsUserAuthorized(user.Identity.Name, _someFilterParameter);
        if (!isAuthorized)
        {
            context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            return;
        }
    }
}
gius
fuente
44
¿Entonces solo puede usar esto para negar la autorización, no para otorgarla ?
MEMark
1
@MEMark Al otorgar , ¿se refiere a anular otro atributo de autorización?
gius
2
AFAIK, el acceso está permitido de forma predeterminada, por lo que debe denegarlo explícitamente (por ejemplo, agregando un AuthorizeAttribute). Consulte esta pregunta para obtener más detalles: stackoverflow.com/questions/17272422/…
gius
16
También tenga en cuenta que, en el ejemplo sugerido, uno no tiene que heredar de AuthorizeAttribute. Puede heredar de Attribute y IAuthorizationFilter . De esta forma, no obtendría la siguiente excepción si se utiliza algún
Anatolyevich
13
Tenga en cuenta que si su OnAuthorizationimplementación necesita esperar un método asíncrono, debe implementar en IAsyncAuthorizationFilterlugar de lo IAuthorizationFiltercontrario, su filtro se ejecutará sincrónicamente y su acción de controlador se ejecutará independientemente del resultado del filtro.
Codemunkie
34

Basado en la GRAN respuesta de Derek Greer , lo hice con enumeraciones.

Aquí hay un ejemplo de mi código:

public enum PermissionItem
{
    User,
    Product,
    Contact,
    Review,
    Client
}

public enum PermissionAction
{
    Read,
    Create,
}


public class AuthorizeAttribute : TypeFilterAttribute
{
    public AuthorizeAttribute(PermissionItem item, PermissionAction action)
    : base(typeof(AuthorizeActionFilter))
    {
        Arguments = new object[] { item, action };
    }
}

public class AuthorizeActionFilter : IAuthorizationFilter
{
    private readonly PermissionItem _item;
    private readonly PermissionAction _action;
    public AuthorizeActionFilter(PermissionItem item, PermissionAction action)
    {
        _item = item;
        _action = action;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        bool isAuthorized = MumboJumboFunction(context.HttpContext.User, _item, _action); // :)

        if (!isAuthorized)
        {
            context.Result = new ForbidResult();
        }
    }
}

public class UserController : BaseController
{
    private readonly DbContext _context;

    public UserController( DbContext context) :
        base()
    {
        _logger = logger;
    }

    [Authorize(PermissionItem.User, PermissionAction.Read)]
    public async Task<IActionResult> Index()
    {
        return View(await _context.User.ToListAsync());
    }
}
bruno.almeida
fuente
1
Gracias por esto. Creé esta publicación con una implementación ligeramente diferente y una solicitud de validación stackoverflow.com/questions/49551047/…
Anton Swanevelder
2
MumboJumboFunction <3
Marek Urbanowicz
31

Puede crear su propio AuthorizationHandler que encontrará atributos personalizados en sus Controladores y Acciones, y pasarlos al método HandleRequirementAsync.

public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
    {
        var attributes = new List<TAttribute>();

        var action = (context.Resource as AuthorizationFilterContext)?.ActionDescriptor as ControllerActionDescriptor;
        if (action != null)
        {
            attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
            attributes.AddRange(GetAttributes(action.MethodInfo));
        }

        return HandleRequirementAsync(context, requirement, attributes);
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

    private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo)
    {
        return memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
    }
}

Luego puede usarlo para cualquier atributo personalizado que necesite en sus controladores o acciones. Por ejemplo, para agregar requisitos de permisos. Simplemente cree su atributo personalizado.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : AuthorizeAttribute
{
    public string Name { get; }

    public PermissionAttribute(string name) : base("Permission")
    {
        Name = name;
    }
}

Luego cree un Requisito para agregar a su Política

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    //Add any custom requirement properties if you have them
}

Luego cree el AuthorizationHandler para su atributo personalizado, heredando el AttributeAuthorizationHandler que creamos anteriormente. Se pasará un IEnumerable para todos sus atributos personalizados en el método HandleRequirementsAsync, acumulado desde su Controlador y Acción.

public class PermissionAuthorizationHandler : AttributeAuthorizationHandler<PermissionAuthorizationRequirement, PermissionAttribute>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement, IEnumerable<PermissionAttribute> attributes)
    {
        foreach (var permissionAttribute in attributes)
        {
            if (!await AuthorizeAsync(context.User, permissionAttribute.Name))
            {
                return;
            }
        }

        context.Succeed(requirement);
    }

    private Task<bool> AuthorizeAsync(ClaimsPrincipal user, string permission)
    {
        //Implement your custom user permission logic here
    }
}

Y, por último, en su método Startup.cs ConfigureServices, agregue su AuthorizationHandler personalizado a los servicios y agregue su Política.

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Permission", policyBuilder =>
            {
                policyBuilder.Requirements.Add(new PermissionAuthorizationRequirement());
            });
        });

Ahora puede simplemente decorar sus Controladores y Acciones con su atributo personalizado.

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}
Shawn
fuente
1
Voy a echar un vistazo a esto lo antes posible.
NathanAldenSr
55
Esto está bastante diseñado ... Resolví lo mismo usando un simple AuthorizationFilterAttribute que recibe un parámetro. No necesita reflexión para esto, parece aún más artificial que la solución "oficial" (que me parece bastante pobre).
Vi100
2
@ Vi100 No pude encontrar mucha información sobre AuthorizationFilters en ASP.NET Core. La página de documentación oficial dice que actualmente están trabajando en este tema. docs.microsoft.com/en-us/aspnet/core/security/authorization/…
Shawn
44
@ Vi100 ¿Puede compartir su solución? Si hay una manera más simple de lograr esto, me encantaría saberlo.
Shawn
2
Una cosa a tener en cuenta es que el uso de UnderlyingSystemType anterior no se compila, pero eliminarlo parece funcionar.
hora del té
25

¿Cuál es el enfoque actual para hacer un AuthorizeAttribute personalizado?

Fácil: no cree el suyo propio AuthorizeAttribute.

Para escenarios de autorización pura (como restringir el acceso solo a usuarios específicos), el enfoque recomendado es usar el nuevo bloque de autorización: https://github.com/aspnet/MusicStore/blob/1c0aeb08bb1ebd846726232226279bbe001782e1/samples/MusicStore/Startup.cs#L84 -L92

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<AuthorizationOptions>(options =>
        {
            options.AddPolicy("ManageStore", policy => policy.RequireClaim("Action", "ManageStore"));
        });
    }
}

public class StoreController : Controller
{
    [Authorize(Policy = "ManageStore"), HttpGet]
    public async Task<IActionResult> Manage() { ... }
}

Para la autenticación, se maneja mejor en el nivel de middleware.

¿Qué estás tratando de lograr exactamente?

Kévin Chalet
fuente
1
Recibo una ID de sesión en la Autorización de encabezado. A partir de ese ID sabré si una acción en particular es válida.
jltrem
1
Entonces eso no es un problema de autorización. Supongo que su "ID de sesión" es en realidad un token que contiene la identidad de la persona que llama: esto definitivamente debe hacerse en el nivel de middleware.
Kévin Chalet
3
No es autenticación (establecer quién es el usuario) sino autorización (determinar si un usuario debe tener acceso a un recurso). Entonces, ¿dónde sugieres que busque resolver esto?
jltrem
3
@jltrem, de acuerdo, de lo que estás hablando es de autorización, no de autenticación.
bopapa_1979
2
@Pinpoint no lo soy. Consulto otro sistema para esa información. Ese sistema se autentica (determina el usuario) y autoriza (me dice a qué puede acceder ese usuario). En este momento lo he pirateado para que funcione al llamar a un método en cada acción del controlador para que el otro sistema verifique la sesión. Me gustaría que esto suceda automáticamente a través de un atributo.
jltrem
4

Si alguien solo quiere validar un token de portador en la fase de autorización utilizando las prácticas de seguridad actuales que puede,

agregue esto a su Startup / ConfigureServices

    services.AddSingleton<IAuthorizationHandler, BearerAuthorizationHandler>();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();

    services.AddAuthorization(options => options.AddPolicy("Bearer",
        policy => policy.AddRequirements(new BearerRequirement())
        )
    );

y esto en tu base de código,

public class BearerRequirement : IAuthorizationRequirement
{
    public async Task<bool> IsTokenValid(SomeValidationContext context, string token)
    {
        // here you can check if the token received is valid 
        return true;
    }
}

public class BearerAuthorizationHandler : AuthorizationHandler<BearerRequirement> 
{

    public BearerAuthorizationHandler(SomeValidationContext thatYouCanInject)
    {
       ...
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BearerRequirement requirement)
    {
        var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
        string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.Contains("Bearer"))
        {
            var token = authHeader.Replace("Bearer ", string.Empty);
            if (await requirement.IsTokenValid(thatYouCanInject, token))
            {
                context.Succeed(requirement);
            }
        }
    }
}

Si el código no llega context.Succeed(...) , fallará de todos modos (401).

Y luego en tus controladores puedes usar

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Gabriel P.
fuente
¿Por qué elegirías realizar tu propia validación del token cuando el middleware JwtBearer ya se encarga de esto? También pone el contenido correcto en el encabezado de respuesta WWW-Authenticate para una falla de validación / caducidad de token / autenticación. Si desea acceder a la canalización de autenticación, hay eventos específicos que puede aprovechar en las opciones AddJwtBearer (OnAuthenticationFailed, OnChallenge, OnMessageReceived y OnTokenValidated).
Darren Lewis
Esto es infinitamente más simple que cualquier otra solución que haya visto. Especialmente para casos de uso de clave de API simple. Una actualización: para 3.1, la conversión a AuthorizationFilterContext ya no es válida debido al material de enrutamiento del punto final. Debe tomar el contexto a través de HttpContextAccessor.
JasonCoder
2

La forma moderna es AuthenticationHandlers

en startup.cs agregar

services.AddAuthentication("BasicAuthentication").AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IUserService _userService;

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IUserService userService)
            : base(options, logger, encoder, clock)
        {
            _userService = userService;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Missing Authorization Header");

            User user = null;
            try
            {
                var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
                var username = credentials[0];
                var password = credentials[1];
                user = await _userService.Authenticate(username, password);
            }
            catch
            {
                return AuthenticateResult.Fail("Invalid Authorization Header");
            }

            if (user == null)
                return AuthenticateResult.Fail("Invalid User-name or Password");

            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            };
            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
    }

IUserService es un servicio que realiza cuando tiene nombre de usuario y contraseña. básicamente, devuelve una clase de usuario que usa para asignar sus reclamos.

var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            }; 

Luego puede consultar estos reclamos y cualquier dato que haya mapeado, hay bastantes, eche un vistazo a la clase ClaimTypes

puede usar esto en un método de extensión y obtener cualquiera de las asignaciones

public int? GetUserId()
{
   if (context.User.Identity.IsAuthenticated)
    {
       var id=context.User.FindFirst(ClaimTypes.NameIdentifier);
       if (!(id is null) && int.TryParse(id.Value, out var userId))
            return userId;
     }
      return new Nullable<int>();
 }

Esta nueva forma, creo que es mejor que

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.Headers.Authorization != null)
        {
            var authToken = actionContext.Request.Headers.Authorization.Parameter;
            // decoding authToken we get decode value in 'Username:Password' format
            var decodeauthToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authToken));
            // spliting decodeauthToken using ':'
            var arrUserNameandPassword = decodeauthToken.Split(':');
            // at 0th postion of array we get username and at 1st we get password
            if (IsAuthorizedUser(arrUserNameandPassword[0], arrUserNameandPassword[1]))
            {
                // setting current principle
                Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(arrUserNameandPassword[0]), null);
            }
            else
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            }
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }

    public static bool IsAuthorizedUser(string Username, string Password)
    {
        // In this method we can handle our database logic here...
        return Username.Equals("test") && Password == "test";
    }
}
Walter Vehoeven
fuente
¡Esta respuesta brillante simplemente funciona como un encanto! Gracias por eso y le deseo que sea votado, ya que es la mejor respuesta que he encontrado después de seis horas de búsqueda en blogs, documentación y pila para la autenticación básica más la autorización de roles.
Piotr Śródka
@ PiotrŚródka, de nada, tenga en cuenta que la respuesta es un poco "simplificada", pruebe si tiene un ':' en el texto ya que un usuario malintencionado podría intentar bloquear su servicio simplemente no jugando bien terminando en un índice de excepción de rango. como siempre prueba lo que te dan fuentes externas
Walter Vehoeven
2

Al momento de escribir este artículo, creo que esto se puede lograr con la interfaz IClaimsTransformation en asp.net core 2 y superior. Acabo de implementar una prueba de concepto que es lo suficientemente compartible para publicar aquí.

public class PrivilegesToClaimsTransformer : IClaimsTransformation
{
    private readonly IPrivilegeProvider privilegeProvider;
    public const string DidItClaim = "http://foo.bar/privileges/resolved";

    public PrivilegesToClaimsTransformer(IPrivilegeProvider privilegeProvider)
    {
        this.privilegeProvider = privilegeProvider;
    }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity is ClaimsIdentity claimer)
        {
            if (claimer.HasClaim(DidItClaim, bool.TrueString))
            {
                return principal;
            }

            var privileges = await this.privilegeProvider.GetPrivileges( ... );
            claimer.AddClaim(new Claim(DidItClaim, bool.TrueString));

            foreach (var privilegeAsRole in privileges)
            {
                claimer.AddClaim(new Claim(ClaimTypes.Role /*"http://schemas.microsoft.com/ws/2008/06/identity/claims/role" */, privilegeAsRole));
            }
        }

        return principal;
    }
}

Para usar esto en su controlador, simplemente agregue un apropiado [Authorize(Roles="whatever")]a sus métodos.

[HttpGet]
[Route("poc")]
[Authorize(Roles = "plugh,blast")]
public JsonResult PocAuthorization()
{
    var result = Json(new
    {
        when = DateTime.UtcNow,
    });

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

En nuestro caso, cada solicitud incluye un encabezado de autorización que es un JWT. Este es el prototipo y creo que haremos algo muy parecido a esto en nuestro sistema de producción la próxima semana.

Futuros votantes, consideren la fecha de escritura cuando voten. A partir de hoy, estoworks on my machine. ™ Probablemente desee más manejo de errores e inicio de sesión en su implementación.

Sin reembolsos Sin devoluciones
fuente
¿Qué pasa con ConfigureServices? ¿Es necesario agregar algo?
Daniel
Como se discutió en otra parte, sí.
Sin reembolsos Sin devoluciones
1

Para autorización en nuestra aplicación. Tuvimos que llamar a un servicio basado en los parámetros pasados ​​en el atributo de autorización.

Por ejemplo, si queremos verificar si el médico registrado puede ver las citas de los pacientes, pasaremos "View_Appointment" para personalizar el atributo de autorización y lo verificaremos directamente en el servicio de base de datos y en función de los resultados que autorizaremos. Aquí está el código para este escenario:

    public class PatientAuthorizeAttribute : TypeFilterAttribute
    {
    public PatientAuthorizeAttribute(params PatientAccessRights[] right) : base(typeof(AuthFilter)) //PatientAccessRights is an enum
    {
        Arguments = new object[] { right };
    }

    private class AuthFilter : IActionFilter
    {
        PatientAccessRights[] right;

        IAuthService authService;

        public AuthFilter(IAuthService authService, PatientAccessRights[] right)
        {
            this.right = right;
            this.authService = authService;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var allparameters = context.ActionArguments.Values;
            if (allparameters.Count() == 1)
            {
                var param = allparameters.First();
                if (typeof(IPatientRequest).IsAssignableFrom(param.GetType()))
                {
                    IPatientRequest patientRequestInfo = (IPatientRequest)param;
                    PatientAccessRequest userAccessRequest = new PatientAccessRequest();
                    userAccessRequest.Rights = right;
                    userAccessRequest.MemberID = patientRequestInfo.PatientID;
                    var result = authService.CheckUserPatientAccess(userAccessRequest).Result; //this calls DB service to check from DB
                    if (result.Status == ReturnType.Failure)
                    {
                        //TODO: return apirepsonse
                        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
                    }
                }
                else
                {
                    throw new AppSystemException("PatientAuthorizeAttribute not supported");
                }
            }
            else
            {
                throw new AppSystemException("PatientAuthorizeAttribute not supported");
            }
        }
    }
}

Y en la acción API lo usamos así:

    [PatientAuthorize(PatientAccessRights.PATIENT_VIEW_APPOINTMENTS)] //this is enum, we can pass multiple
    [HttpPost]
    public SomeReturnType ViewAppointments()
    {

    }
Abdullah
fuente
1
Tenga en cuenta que IActionFilter será un problema cuando desee utilizar el mismo atributo para los métodos Hub en SignalR.SignalR Hubs esperan IAuthorizationFilter
ilkerkaran
Gracias por la info. No estoy usando SignalR en mi aplicación en este momento, así que no lo he probado con él.
Abdullah
Supongo que el mismo principio, ya que aún tendrá que usar la entrada de autorización del encabezado, la implementación será diferente
Walter Vehoeven,
0

La respuesta aceptada ( https://stackoverflow.com/a/41348219/4974715 ) no es mantenible o adecuada de manera realista porque "CanReadResource" se está utilizando como un reclamo (pero esencialmente debería ser una política en realidad, IMO). El enfoque en la respuesta no está bien en la forma en que se usó, porque si un método de acción requiere muchas configuraciones de reclamos diferentes, entonces con esa respuesta tendría que escribir repetidamente algo como ...

[ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")] 
[ClaimRequirement(MyClaimTypes.AnotherPermision, "AnotherClaimVaue")]
//and etc. on a single action.

Entonces, imagine la cantidad de codificación que tomaría. Idealmente, se supone que "CanReadResource" es una política que utiliza muchos reclamos para determinar si un usuario puede leer un recurso.

Lo que hago es crear mis políticas como una enumeración y luego recorrer y configurar los requisitos de esta manera ...

services.AddAuthorization(authorizationOptions =>
        {
            foreach (var policyString in Enum.GetNames(typeof(Enumerations.Security.Policy)))
            {
                authorizationOptions.AddPolicy(
                    policyString,
                    authorizationPolicyBuilder => authorizationPolicyBuilder.Requirements.Add(new DefaultAuthorizationRequirement((Enumerations.Security.Policy)Enum.Parse(typeof(Enumerations.Security.Policy), policyWrtString), DateTime.UtcNow)));

      /* Note that thisn does not stop you from 
          configuring policies directly against a username, claims, roles, etc. You can do the usual.
     */
            }
        }); 

La clase DefaultAuthorizationRequirement se parece a ...

public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
    public Enumerations.Security.Policy Policy {get; set;} //This is a mere enumeration whose code is not shown.
    public DateTime DateTimeOfSetup {get; set;} //Just in case you have to know when the app started up. And you may want to log out a user if their profile was modified after this date-time, etc.
}

public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{
    private IAServiceToUse _aServiceToUse;

    public DefaultAuthorizationHandler(
        IAServiceToUse aServiceToUse
        )
    {
        _aServiceToUse = aServiceToUse;
    }

    protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
    {
        /*Here, you can quickly check a data source or Web API or etc. 
           to know the latest date-time of the user's profile modification...
        */
        if (_aServiceToUse.GetDateTimeOfLatestUserProfileModication > requirement.DateTimeOfSetup)
        {
            context.Fail(); /*Because any modifications to user information, 
            e.g. if the user used another browser or if by Admin modification, 
            the claims of the user in this session cannot be guaranteed to be reliable.
            */
            return;
        }

        bool shouldSucceed = false; //This should first be false, because context.Succeed(...) has to only be called if the requirement specifically succeeds.

        bool shouldFail = false; /*This should first be false, because context.Fail() 
        doesn't have to be called if there's no security breach.
        */

        // You can do anything.
        await doAnythingAsync();

       /*You can get the user's claims... 
          ALSO, note that if you have a way to priorly map users or users with certain claims 
          to particular policies, add those policies as claims of the user for the sake of ease. 
          BUT policies that require dynamic code (e.g. checking for age range) would have to be 
          coded in the switch-case below to determine stuff.
       */

        var claims = context.User.Claims;

        // You can, of course, get the policy that was hit...
        var policy = requirement.Policy

        //You can use a switch case to determine what policy to deal with here...
        switch (policy)
        {
            case Enumerations.Security.Policy.CanReadResource:
                 /*Do stuff with the claims and change the 
                     value of shouldSucceed and/or shouldFail.
                */
                 break;
            case Enumerations.Security.Policy.AnotherPolicy:
                 /*Do stuff with the claims and change the 
                    value of shouldSucceed and/or shouldFail.
                 */
                 break;
                // Other policies too.

            default:
                 throw new NotImplementedException();
        }

        /* Note that the following conditions are 
            so because failure and success in a requirement handler 
            are not mutually exclusive. They demand certainty.
        */

        if (shouldFail)
        {
            context.Fail(); /*Check the docs on this method to 
            see its implications.
            */
        }                

        if (shouldSucceed)
        {
            context.Succeed(requirement); 
        } 
     }
}

Tenga en cuenta que el código anterior también puede habilitar la asignación previa de un usuario a una política en su almacén de datos. Por lo tanto, al redactar reclamos para el usuario, básicamente recupera las políticas que se habían asignado previamente al usuario directa o indirectamente (por ejemplo, porque el usuario tiene un cierto valor de reclamo y ese valor de reclamo se ha identificado y asignado a una política, como que proporciona un mapeo automático para los usuarios que también tienen ese valor de reclamo), y alista las políticas como reclamos, de modo que en el controlador de autorización, simplemente puede verificar si los reclamos del usuario contienen requisitos. reclamación (es. Esto es para una forma estática de satisfacer un requisito de política, por ejemplo, el requisito de "nombre" es de naturaleza bastante estática. Entonces,

[Authorize(Policy = nameof(Enumerations.Security.Policy.ViewRecord))] 

Un requisito dinámico puede ser verificar el rango de edad, etc. y las políticas que usan dichos requisitos no pueden asignarse previamente a los usuarios.

En la respuesta dada por @blowdart ( https://stackoverflow.com/a/31465227/4974715 ) ya se encuentra un ejemplo de verificación dinámica de reclamos de políticas (por ejemplo, para verificar si un usuario tiene más de 18 años ).

PD: escribí esto en mi teléfono. Disculpe cualquier error tipográfico y falta de formato.

Olumide
fuente