REST API estrategias de autorización

7

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

Por ejemplo, digamos 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 al método getAuthenticatedUser() que devolverá al usuario nulo si el usuario no ha iniciado sesión, o un objeto de dominio de usuario que representa al usuario registrado .

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

Supongamos que hay varios objetos de dominio diferentes que se pueden asociar 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 a /api/invoices/{invoice_id} , necesitamos verificar que el usuario esté autorizado para acceder a ese recurso antes de que podamos realizar el servicio.

Mi pregunta es, entonces, ¿existen patrones o estrategias para enfrentar este problema de autorización? Una opción que estoy considerando es crear una interfaz de ayuda (es decir, SecurityUtils.isUserAuthorized(user, object) ), que se puede llamar durante el procesamiento de la solicitud para asegurar que el usuario esté autorizado para recuperar el objeto. Esto no es ideal, ya que contamina el código del 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, lo que podría ser un poco molesto. Esta podría ser la única opción, ¡pero me gustaría escuchar sus sugerencias!

    
pregunta HJCee 01.08.2016 - 14:39

3 respuestas

7

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

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

En general, lo que hago, al menos en Ruby on Rails, es hacer 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 nada parecido 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 ser "propietarios" de los recursos anidados, por lo que la lógica de nivel superior es inherentemente inútil. 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 objeto Course . No es una aplicación User -centric. Sin embargo, User s está inscrito en Course , así que simplemente puedo asegurar que:

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

para cualquier recurso que un determinado User necesita modificar, ya que casi todos los recursos están vinculados a un Course . Esto se hace en la clase de política en el método owns_whatever .

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

Las acciones de tu controlador podrían verse algo así como:

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);
}
    
respondido por el Chris Cirefice 04.08.2016 - 09:32
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 de negocio y puede ser manejado por Spring Security o un código AOP personalizado. Si usa estas anotaciones en sus métodos de negocio 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.

    
respondido por el Michał Kosmulski 01.08.2016 - 15:15
0

Utilice la seguridad basada en capacidades.

Una capacidad es un objeto imperdonable que actúa como evidencia de que se 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 interfaces respectivas. Estos deberían lanzar 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

    
respondido por el Demi 14.08.2016 - 23:01

Lea otras preguntas en las etiquetas