Estrategias de autorización API REST

8

Aquí hay muchas preguntas que tratan sobre la mecánica de autenticación y autorización de las API RESTful, pero ninguna de ellas parece entrar en detalles sobre cómo implementar servicios seguros a nivel de aplicación.

Por ejemplo, supongamos que mi aplicación web (tengo en mente Java pero esto se aplica a cualquier backend) tiene un sistema de autenticación seguro que permite a los usuarios de la API iniciar sesión con un nombre de usuario y contraseña. Cuando el usuario realiza una solicitud, en cualquier momento durante el proceso de procesamiento de la solicitud, puedo llamar a un getAuthenticatedUser()método que devolverá el usuario nulo si el usuario no ha iniciado sesión o un objeto de dominio de Usuario que representa al usuario conectado.

La API permite a los usuarios autenticados acceder a sus datos, por ejemplo, GET para /api/orders/devolver la lista de pedidos de ese usuario. Del mismo modo, GET to /api/tasks/{task_id}devolverá datos relacionados con esa tarea específica.

Supongamos que hay varios objetos de dominio diferentes que pueden asociarse con la cuenta de un usuario (los pedidos y las tareas son dos ejemplos, también podríamos tener clientes, facturas, etc.). Solo queremos que los usuarios autenticados puedan acceder a los datos sobre sus propios objetos, por lo que cuando un usuario realiza una llamada, /api/invoices/{invoice_id}debemos verificar que el usuario esté autorizado para acceder a ese recurso antes de que lo sirvamos.

Mi pregunta es, entonces, ¿existen patrones o estrategias para tratar este problema de autorización? Una opción que estoy considerando es crear una interfaz auxiliar (es decir SecurityUtils.isUserAuthorized(user, object)), a la que se pueda llamar durante el procesamiento de la solicitud para garantizar que el usuario esté autorizado para recuperar el objeto. Esto no es ideal ya que contamina el código de punto final de la aplicación con muchas de estas llamadas, por ejemplo

Object someEndpoint(int objectId) {
    if (!SecurityUtils.isUserAuthorized(loggedInUser, objectDAO.get(objectId)) {
        throw new UnauthorizedException();
    }
    ...
}

... y luego está la cuestión de implementar este método para cada tipo de dominio que podría ser un poco difícil. ¡Esta podría ser la única opción, pero me interesaría escuchar sus sugerencias!

HJCee
fuente
Cuando dice que los usuarios "inician sesión", ¿quiere decir que está manteniendo una sesión?
JimmyJames

Respuestas:

8

Por favor, por el amor de Dios, ¡no cree una SecurityUtilsclase!

¡Tu clase se convertirá en 10k líneas de código de espagueti en cuestión de meses! Debería tener un Actiontipo (crear, leer, actualizar, destruir, enumerar, etc.) pasado a su isUserAuthorized()método, que rápidamente se convertiría en una switchdeclaración de mil líneas con una lógica cada vez más compleja que sería difícil de probar. No lo hagas


En general, lo que hago, al menos en Ruby on Rails, es que cada objeto de dominio sea responsable de sus propios privilegios de acceso al tener una clase de política para cada modelo . Luego, el controlador pregunta a la clase de política si el usuario actual para la solicitud tiene acceso al recurso o no. Aquí hay un ejemplo en Ruby, ya que nunca he implementado algo así en Java, pero la idea debería ser clara:

class OrderPolicy

    class Scope < Struct.new(:user, :scope)

        def resolve

            # A user must be logged in to interact with this resource at all
            raise NotAuthorizedException unless user

            # Admin/moderator can see all orders
            if (user.admin? || user.moderator?)
                scope.all
            else
                # Only allow the user to see their own orders
                scope.where(orderer_id: user.id)
            end
        end
    end

    # Constructor, if you don't know Ruby
    def initialize(user, order)
        raise NotAuthorizedException unless user
        @user = user
        @order= order
    end

    # Whitelist what data can be manipulated by each type of user
    def valid_attributes
        if @user.admin?
            [:probably, :want, :to, :let, :admin, :update, :everything]
        elsif @user.moderator?
            [:fewer, :attributes, :but, :still, :most]
        else
            [:regualar, :user, :attributes]
        end
    end

    # Maybe restrict updatable attributes further
    def valid_update_attributes
    end

    # Who can create new orders
    def create?
        true # anyone, and they would have been authenticated already by #initialize
    end

    # Read operation
    def show?
        @user.admin? || @user.moderator? || owns_order
    end

    # Only superusers can update resources
    def update?
        @user.admin? || @user.moderator?
    end

    # Only admins can delete, because it's extremely destructive or whatever
    def destroy?
        @user.admin?
    end

    private

    # A user 'owns' an order if they were the person who submitted the order
    # E.g. superusers can access the order, but they didn't create it
    def owns_order
        @order.orderer_id == @user.id
    end
end

Incluso si tiene recursos anidados complejos, algunos recursos deben 'poseer' los recursos anidados, por lo que la lógica de nivel superior inherentemente se dispara. Sin embargo, esos recursos anidados necesitan sus propias clases de políticas en caso de que puedan actualizarse independientemente del recurso 'principal'.

En mi solicitud, que es para el departamento de mi universidad, todo gira en torno al Courseobjeto. No es una Useraplicación céntrica. Sin embargo, los Users están inscritos Course, así que simplemente puedo asegurarme de que:

@course.users.include? current_user && (whatever_other_logic_I_need)

para cualquier recurso que un particular Usernecesite modificar, ya que casi todos los recursos están vinculados a a Course. Esto se hace en la clase de política del owns_whatevermétodo.

No he hecho mucha arquitectura Java, pero parece que podrías hacer una Policyinterfaz, donde los diferentes recursos que necesitan ser autenticados deben implementar la interfaz. Luego, tiene todos los métodos necesarios que pueden volverse tan complejos como los necesita por objeto de dominio . Lo importante es vincular la lógica al modelo en sí, pero al mismo tiempo mantenerla en una clase separada (principio de responsabilidad única (SRP)).

Las acciones de su controlador podrían verse así:

public List<Order> index(OrderQuery query) {

    authorize(Order.class)
    // you should be auto-rescuing the NotAuthorizedException thrown by
    //the policy class at the controller level (or application level)

    // if the authorization didn't fail/rescue from exception, just render the resource
    List<Order> orders = db.search(query);
    return renderJSON(orders);
}

public Order show(int orderId) {

    authorize(Order.class)
    Order order = db.find(orderId);
    return renderJSON(order);
}
Chris Cirefice
fuente
1

Una solución más conveniente es usar anotaciones para marcar métodos que requieren algún tipo de autorización. Esto se destaca de su código comercial y puede ser manejado por Spring Security o código AOP personalizado. Si utiliza estas anotaciones en sus métodos comerciales en lugar de puntos finales, puede estar seguro de obtener una excepción cuando un usuario no autorizado intente llamarlos independientemente del punto de entrada.

Michał Kosmulski
fuente
No es lo que estoy preguntando. Las anotaciones de Spring le permiten garantizar que el usuario tenga un nivel de autorización particular (por ejemplo, que el usuario es un administrador), pero no creo que ayuden a restringir el acceso a entidades particulares. Digamos que la lógica de negocios recupera y devuelve una factura basada en una identificación. Me conecto y proporciono el ID de la factura de otra persona al punto final. ¿No creo que las anotaciones de Spring impidan el acceso horizontal de esa forma?
HJCee
1
Las anotaciones de @HJCee Spring Securities AOP son bastante expresivas . Puede definir una @PreAuthorization("hasRole('ADMIN') and #requestingUser.company.uuid == authentication.details.companyUuid")anotación en la que el #requestingUsersegmento haga referencia a un objeto pegado con fieldName requestingUserque tiene un método getCompany()cuyo objeto devuelto tiene otro método getUuid(). Se authenticationrefiere al Authenticationobjeto almacenado en el contexto de seguridad.
Roman Vottner
1
@RomanVottner ¿Qué sucede si necesita una autorización realmente compleja? por ejemplo, solo los moderadores en Stack Exchange con insignias de oro X en la etiqueta Y pueden realizar ediciones para eliminar preguntas (o lo que sea). Transmitiré la anotación de una sola línea de 300 caracteres.
Chris Cirefice
1
@ChrisCirefice Utilice @PreAuthorize("hasPermission(#user, 'allowDoSomething')")e implemente su evaluador de permisos personalizado o escriba un controlador de expresión personalizado y una raíz . Si desea modificar el comportamiento de las anotaciones disponibles, eche un vistazo a este hilo
Roman Vottner
0

Use seguridad basada en capacidades.

Una capacidad es un objeto inolvidable que actúa como evidencia de que uno puede realizar una determinada acción. En este caso:

  • Haga que cada rol (conjunto de acciones permitidas) sea una interfaz.
  • Haga que las operaciones que requieren autenticación sean métodos en sus respectivas interfaces. Estos deberían arrojar una excepción si el receptor no es el usuario actual de la solicitud, si es posible.

Esto hace que sea imposible intentar hacer algo que el usuario actual no está autorizado a hacer.

De esa manera es imposible

Demi
fuente
1
No para ser el crítico, pero ¿no es esto un TL; DR de mi respuesta? En caso afirmativo, sería preferible simplemente comentar mi respuesta en lugar de escribir la suya :)
Chris Cirefice
No exactamente. La idea aquí es expresar los diferentes roles que los usuarios pueden tener en el sistema de tipos Java, de modo que no se pueda llamar a un método en un usuario que requiere un privilegio que el usuario no tiene.
Demi el
Además del comentario de Chris, mi pregunta no es sobre la restricción de acceso basada en roles (que es trivial de implementar con cualquier buen marco web) sino sobre restricciones de acceso basadas en asociaciones entre usuarios y datos ('es objeto X propiedad del usuario Y' es un ejemplo realmente simple de tal asociación pero podrían ser muy complejos). Ese es el problema sobre el que realmente estoy tratando de obtener consejos.
HJCee