maurobonfietti / slim4-api-skeleton

Useful skeleton for RESTful API development with PHP and Slim 4.
http://bit.ly/2nNNOZi
MIT License
132 stars 32 forks source link

Role based authorization #57

Closed devlamine closed 2 years ago

devlamine commented 3 years ago

Hello, I hope all is well with you.

What is the best way to implement a role based authentication and authorization system in slim4-api-skeleton

My app will have three different users - (admin, Author,Editor ...). I want to configure allowed groups to a route ?

Thanks

DLzer commented 3 years ago

How you authenticate users to the API is one thing. That depends on you're design completely. As for authorizing access to routes based on specific roles I believe the preferred pattern is to create your own middleware for RoleBasedAuth. Then in the Routes.php you can specify which routes to add your middleware to: $app->get('/adminsOnly', ...)->add(AdminOnlyAuth::class);

devlamine commented 3 years ago

Thank you for your answer @DLzer . I have several access levels not only the admin, I have to create each role = midelware ? this is not practical ! sometimes there are routes returned a response depending on the connected user example if (is_admin ()) { return all data } else if (is_editor () { list of editors } ....

I think of it this way: in the middleware I get the role $request = $request-> withAttribute ('role', $role); ?

DLzer commented 3 years ago

I understand, you're looking more-so at authenticating the route via middleware. This is ultimately up to you. However, I'll share my approach which is the typical blog method. Lets say you have 3 roles ( Admin, Editor, Reader ). You can implement middleware which will allow 'Role and Above' to access your route.

At the Reader level, locking out routes from Editors, or Admins is typically unnecessary so you can omit the role specific middleware and just authenticate that the user is who they say they are.

For the Editor level you really only want to lock out Readers.. considering Admins should still be able to access all areas that editors can. So you can have EditorMiddleware that specifies only (Editor, Admin) have the capability to access the route, while still authenticating that they are who they say they are.

And for Admins, obviously you want to lock out anyone who is not an admin under AdminMiddleware.

In terms of 'how' you authenticate is up to you. I typically sent a JWT on authentication, and verify/authorize that upon each request to the API. So in theory

I hope this was helpful and/or made sense.

devlamine commented 3 years ago

Hi @DLzer , you answered a part of my question which 'is already implemented but not for a route which returns an answer and which must be changed according to the role of the authenticated user. I give you an example maybe I misspoke: I have a route which returns the list of students if a role teacher the route should only return the students of that teacher and if a role director the route should return all the students of the school. Do you recommend the solution to pass the role in the header in the Middleware and process the response according to the role?

in Middleware

$request = $request->withAttribute('role', $role); ?

in Route

$role= $request->getAttribute('role')

if($role==='teacher'){
  //get list students of that teacher
}

if($role==='director'){
  //get list all the students of the school.
}

Thank you for your advice !

DLzer commented 3 years ago

In my opinion I would not append attributes at the middleware level. You want to keep each component as simple as possible while following the single responsibility principle. In that case I would have by database set up like:

x_students x_teachers x_students_to_teachers

With the x_students_to_teachers being a join based on a constraint column like x_students.teacher_id = x.teachers.id. Then I would have the routes set up like so:

/students/{id} -> StudentReaderAction -> StudentReaderService <- Returns a single student /students/teacher/{id} -> StudentFinderAction -> StudentFinderService <- Returns a list of students based on teacher ID /students -> StudentFinderAction -> StudentFinderService <- Returns a list of all students

Where the StudentFinderService is more-so parsing a constant ( the route ) instead of accepting something that is variable a role attribute.

Then your middleware could look like:

$app->group('/students', function( RouteCollectorProxy( $app ) {

    // Only director ( or above ) can access
    $app->get('', App\Action\StudentFinderAction::class)->add(DirectorMiddleware);
    // Only specific teacher ( or above ) can access
    $app->get('/teacher/{id}', App\Action\StudentFinderAction::class)->add(TeacherMiddleware);

});

Hope that clarified a bit more.

devlamine commented 3 years ago

Yes I like that it is cleaner but it will generate another problem lol, side app client I have to check if a director? I call the route /students/and if a teacher I call the function /students/teacher/{id}?

DLzer commented 3 years ago

I'm sorry I can't comment to much on client-side development. However, I can say while working with my front end devs that typically you would want to use the JWT returned from authenticating the user to determine role, and create 'Role dependent components' based off of that. An Admin should have a different dashboard then a Directory, and same with a Director verses a Teacher.

Edit to clarify a little more thoroughly:

If you're using something like Angular or React you should have a service that manages the users state throughout the app. Your components can be layers accordingly to the role given by the user scope:

In your DashboardComponent you can determine which role the apps state is holding onto using your role service and pull in the correct component that way.

userService.getById(currentUser.id).then(userFromApi => this.setState({ userFromApi }));

render() {
        const { currentUser, userFromApi } = this.state;
        return(
          { if ( currentUser.role === constants.ADMIN_ROLE )  { return(AdminComponent) }  }
          { if ( currentUser.role === constants.DIRECTOR_ROLE)  { return(DirectorComponent) }
          { if ( currentUser.role === constants.TEACHER_ROLE)  { return(TeacherComponent) }  
        )
}

This react example is obviously stripped down to show proof of concept.

devlamine commented 3 years ago

Great job doing such clean middleware, components, thank you!