3

I am working on a Spring Boot RESTful application which will be exposing a bunch of APIs for the web app to perform CRUD operations on the resources.

I am using spring-data-rest (along with spring-data-jpa of course) to expose the entities/repositories with the help of Spring Magic.

Even though I have secured (role-based) the endpoints with spring-security, it is not completely secure.

For example: I have a User entity with has one-to-many relationship with Car. So the endpoint (auto exposed by spring-data-rest) for getting a user's cars is localhost:8080/users/{userId}/cars

However, any user with the required role can just pass the userId of another user and still access the endpoint. The behavior I want is to secure these endpoints in a way that if I a logged-in user's ID is 1, then we can only hit localhost:8080/users/1/cars. Any other request with any other userId should end up in 403 or something.

Note: I know if write my own controllers then I can get a handle of the principal and do what I desire. I just want to know is there a way or pattern in spring-data-rest to achieve this?

Soumitri Pattnaik
  • 3,246
  • 4
  • 24
  • 42
  • 1
    You need to write Intercepter for that – Vinay Hegde Jan 11 '20 at 15:21
  • @VinayHegde can you please elaborate on this or point me in the right direction? – Soumitri Pattnaik Jan 11 '20 at 15:22
  • Seems unusual that you have `userId` as the path variable. Why not use the logged-in user's id? Also see [Spring Data & Spring Security Configuration](https://docs.spring.io/spring-security/site/docs/5.3.0.M1/reference/htmlsingle/#data-configuration) to use logged-in user's id in JPA queries. – Ritesh Jan 11 '20 at 15:33
  • @Ritesh that endpoint is not a custom controller, that has been automatically exposed by spring-data-rest. – Soumitri Pattnaik Jan 11 '20 at 15:37
  • I would recommend repository customization. See [Spring Data REST filtering data based on the user](https://stackoverflow.com/questions/23640487/) for examples using `@RepositoryRestResource` and `SecurityEvaluationContextExtension`. – Ritesh Jan 11 '20 at 16:26
  • Does this answer your question? [Spring Data Rest: Return Resources of User](https://stackoverflow.com/questions/30834138/spring-data-rest-return-resources-of-user) – Lubo Apr 07 '20 at 07:53

3 Answers3

1

Since you have already secured the application with Spring Security , here is another alternative with Method Security Expressions

Please review the @Pre and @Post Annotations for your requirement.

  1. You may store the logged-in user's userId to the Authentication object.Details here.

  2. Secure the required method with the @PreAuthorize annotation as follows

    @PreAuthorize("#user.userId == authentication.principal.userId")
    public List<Car> getCars(User user){..}
    

Do remember to enable method security

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {..}
R.G
  • 6,436
  • 3
  • 19
  • 28
  • This does not solve question. Question asks for "filtering" results according currently logged user (principal). Possible (note performance) solution can be found also here https://stackoverflow.com/a/30877376/11152683 – Lubo Apr 07 '20 at 07:52
  • @Lubo I couldn't find the "filtering" requirement mentioned in your comment. The requirement is "_The behavior I want is to secure these endpoints in a way that if I a logged-in user's ID is 1, then we can only hit localhost:8080/users/1/cars._" – R.G Apr 07 '20 at 07:58
  • Requirement is there, but it is implicit... According given URL, soumitri-pattnaik does want to make possible getting only cars with association to given user... On the other side, you are right. Utilizing path based security can achieve same behavior. It is than dead simple. Have a look here: https://stackoverflow.com/a/27649847/11152683 – Lubo Apr 07 '20 at 08:49
0

To achieve that you need to write an Interceptor.It will be used under following situation:

  1. Before sending the request to the controller

  2. Before sending the response to the client

Before writing any Interceptor it should implement the HandlerInterceptor interface. Three methods Interceptor supports are :

  1. preHandle() method − Perform operations before sending the request to the controller. This method should return true to return the response to the client.
  2. postHandle() method − Used to perform operations before sending the response to the client.
  3. afterCompletion() method − This is used to perform operations after completing the request and response.

Code :

    @Component
    public class MyInterceptor implements HandlerInterceptor {
       @Override
       public boolean preHandle(
          HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

       String pathVariablesMap = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); 
      //From this pathVariablesMap  extract UserId and match with a loggedinUserId
       }
       @Override
       public void postHandle(
          HttpServletRequest request, HttpServletResponse response, Object handler, 
         ) throws Exception {}

       @Override
       public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
          Object handler, Exception exception) throws Exception {}
    }

By using a InterceptorRegistry you can register your Interceptors like below :

@Component
public class MyRegistoryConfig extends WebMvcConfigurer{
   @Autowired
   MyInterceptor myInterceptor ;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(myInterceptor );
   }
}

For more Info follow this link Interceptors

EDIT : As @Ritesh suggested added that point.

Vinay Hegde
  • 1,424
  • 1
  • 10
  • 23
  • Instead of the header check, OP needs to verify that the value of the `userId` path variable matches with the id of logged in user. One way to do in `preHandle` method of the interceptor is to get the path variables map - something like `Map pathVariablesMap = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);` - and extract the value of `userId` key from that map and compare it with the logged-in user's id. – Ritesh Jan 11 '20 at 16:36
-1

You're using spring security(great :D), so it's better to create a simple filter, register it, then simply do your custom authorize in that filter.

In brief

  1. Create a Custom filter

    • Get userId from the URL path

    • Get userId from SecurityContextHolder (Authenticated user principal)

    • Compare fetched userIds

  2. Register filter in spring security config (After BasicAuthenticationFilter)

1- Create a custom filter (Pseudo-code)

public class CustomFilter extends GenericFilterBean {

@Override
public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain chain) throws IOException, ServletException {

    //Fetch userId from path 
    HttpServletRequest req = (HttpServletRequest) request;
    String path = req.getContextPath();
    //..


    //Fetch userId from SecurityContextHolder (User Principal)
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    User user = (User) authentication.getPrincipal();
    Long userId = user.getId();


    //Compare userId (fethced from path) with authenticated userId (fetched from principal)
    //If comparison is ok
    chain.doFilter(request, response);


    //else
    //throw Unauthorize

}

2- Register a filter in spring security config (After BasicAuthenticationFilter.class)

@Configuration
public class Configuration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterAfter(
            new CustomFilter(), BasicAuthenticationFilter.class);
}
}

With this structure when an authenticated user sends a request, the request will be first checked (Comparison between userIds) and then sent.

More information for creating a filter in spring security:

A Custom Filter in the Spring Security Filter Chain

  • Too complicated. Have a look at PostFilter, PreAuthorize, Query... Note, that Query can use SPEL thing, which does have access to UserPrincipal... – Lubo Apr 07 '20 at 07:55