最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

java - How to prevent Insecure Direct Object Reference (IDOR) using self issued JWT - Stack Overflow

programmeradmin1浏览0评论

I was reading the Spring documentation on authorising HttpServletRequests and JWTs.

My application allows multiple tenants therefore I need to check for every user which tenant they belong to and then allow or disallow the request.

Edit: I fot to mention that I have other micro services using that API which need to be able to access all tenants and users. That is why the path variables are necessary and cannot be replaces by extracting them from JWT claims.

Example:

I have the endpoint: /api/v1/tenants/{tenantId}/users/{userId}/settings

I need to make sure that the User 123 of Tenant 1 can only CRUD their own settings. Neither the settings of other users of the same tenant, nor settings of users of other tenants.

I know that the JWT resource server lets you set certain authorisation checks like shown in this example:

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/contacts/**").access(hasScope("contacts"))
                .requestMatchers("/messages/**").access(hasScope("messages"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}

Or, on the method level:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}

I was thinking of giving all my users a scope SCOPE_TENANT:{tenantId} when issuing their JWT and doing something like this (their username is their email address therefore I can't use @PreAuthorize("#userId == authentication.name"):

@PreAuthorize("hasAuthority('TENANT:' + #tenantId) or hasAuthority('ADMIN')")
@GetMapping("{tenantId}/users/{userId}/settings")
public ResponseEntity<Settings> getTenantResources(@PathVariable long tenantId,
@PathVariable long userId Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    if (user.getId != userId) {
        throw new IdConflicException;
    }
    // Retrieve and return settings from DB using the userId
}

However, I am searching for a better solution, since this would mean a lot of code duplication, and would therefore be error prone and hard to maintain.

Do you know a better solution, like using a filter or a decision in order to make these checks?

Edit:

Thanks to some answers and comments I now settled for a solution that does not involve any method level annotations.

I created a custom utility that extracts the tenantId and userId claims out of the authenticated users JWT with a method to validate the path variables against those.

Then I will manually call this method in each endpoint.

  public static void checkAllowedToAccessTenantAndUser(long tenantId, long userId, Jwt jwt) {
        long jwtUserId = jwt.getClaim("userId");
        long jwtTenantId = jwt.getClaim("tenantId");

        if (scope.contains(Authorities.CLIENT.getAuthority())) {
            return;
        }

        if (jwtUserId != userId || jwtTenantId != tenantId) {
            throw new IdorException();
        }
    }

I was reading the Spring documentation on authorising HttpServletRequests and JWTs.

My application allows multiple tenants therefore I need to check for every user which tenant they belong to and then allow or disallow the request.

Edit: I fot to mention that I have other micro services using that API which need to be able to access all tenants and users. That is why the path variables are necessary and cannot be replaces by extracting them from JWT claims.

Example:

I have the endpoint: /api/v1/tenants/{tenantId}/users/{userId}/settings

I need to make sure that the User 123 of Tenant 1 can only CRUD their own settings. Neither the settings of other users of the same tenant, nor settings of users of other tenants.

I know that the JWT resource server lets you set certain authorisation checks like shown in this example:

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/contacts/**").access(hasScope("contacts"))
                .requestMatchers("/messages/**").access(hasScope("messages"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}

Or, on the method level:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}

I was thinking of giving all my users a scope SCOPE_TENANT:{tenantId} when issuing their JWT and doing something like this (their username is their email address therefore I can't use @PreAuthorize("#userId == authentication.name"):

@PreAuthorize("hasAuthority('TENANT:' + #tenantId) or hasAuthority('ADMIN')")
@GetMapping("{tenantId}/users/{userId}/settings")
public ResponseEntity<Settings> getTenantResources(@PathVariable long tenantId,
@PathVariable long userId Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    if (user.getId != userId) {
        throw new IdConflicException;
    }
    // Retrieve and return settings from DB using the userId
}

However, I am searching for a better solution, since this would mean a lot of code duplication, and would therefore be error prone and hard to maintain.

Do you know a better solution, like using a filter or a decision in order to make these checks?

Edit:

Thanks to some answers and comments I now settled for a solution that does not involve any method level annotations.

I created a custom utility that extracts the tenantId and userId claims out of the authenticated users JWT with a method to validate the path variables against those.

Then I will manually call this method in each endpoint.

  public static void checkAllowedToAccessTenantAndUser(long tenantId, long userId, Jwt jwt) {
        long jwtUserId = jwt.getClaim("userId");
        long jwtTenantId = jwt.getClaim("tenantId");

        if (scope.contains(Authorities.CLIENT.getAuthority())) {
            return;
        }

        if (jwtUserId != userId || jwtTenantId != tenantId) {
            throw new IdorException();
        }
    }
Share Improve this question edited Apr 3 at 0:39 GeekChap asked Mar 31 at 17:52 GeekChapGeekChap 135 bronze badges 2
  • If I understand correctly the problem, you can move all the stuff to a separated filter. For Instance a filter that extends OncePerRequestFilter – Andrei Lisa Commented Apr 1 at 11:29
  • why not write a custom validator for the JWT docs.spring.io/spring-security/reference/servlet/oauth2/… no custom filter – Toerktumlare Commented Apr 1 at 22:15
Add a comment  | 

2 Answers 2

Reset to default 0

This is as simple as writing a custom validator that you insert into your default JWT validation

https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-validation-custom

No need for any custom homemade security filters.

This is something probably best done with a Filter and not using method or class annotations @PreAuthorize. Your JWT integration should already have a filter registered that you just need to slightly modify. Your JWT should possess the tenant ID within it along with the User ID. Then your user has no option to erroneously control the tenant ID or user ID. This means there are far less options for them to manipulate the program to cause you to make a mistake. So if the JWT is the source of this information then...

You should fo having the tenant ID in your URL. This stops someone from sending you bad mojo that you have to defend against, but it also makes it simpler for the API users. The user doesn't have to manage the tenantID or UserID as separate things that need to be added to the URLs. Your URL would go from this:

/api/v1/tenants/{tenantId}/users/{userId}/settings

To this:

/api/v1/settings

This also would mean API users would need to authenticate first to acquire their JWT token.

There is one good reason to keep the tenant ID in your URL which is if users are allowed access to multiple tenants. The tenant ID does provide the ability to select which tenant you want to access. This could be in the URL as you've shown, or it could be at login time and not included in the URL. Possibly a user authenticates to a single tenantID (ie /api/v1/{tenantID}/login). That's a design decision for you to consider. Multi-tenancy might change how you build the JWT claims too as you may need to package all accessible tenants in there (or support wildcards for admins).

You probably already have a custom filter for authenticating the JWT token. To include the tenantID and userID in your JWT token you just need to change your claims like so:

    private String createToken(User user) {
        return Jwts.builder()
                .claims(Map.of("tenantID", user.getTenantID(), 
                               "userID", user.getId())
                .subject(subject)
                .header().empty().add("typ","JWT")
                .and()
                .issuedAt(new Date(System.currentTimeMillis()))
                // 5 minutes expiration time
                .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 50)) 
                .signWith(getSigningKey())
                pact();
    }

Then in your JWT Filter you'd do something like this:

String username = jwtUtil.extractUsername(jwt);
Map<String,Object> claims = jwtUtil.extractClaims(jwt);
Integer tenantID = claims.get("tenantID");
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
MyDetails myDetails = (MyDetails)userDetails;
if( myDetails.getTenantID() == tenantID ) {
    // todo construct the Authorization objects and ...
    SecurityContextHolder.getContext().setAuthentication(auth);
}

In your request method you can do the following now:

@PreAuthorize("...")
@GetMapping("settings")
public ResponseEntity<Settings> getTenantResources(Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    Integer tenantId = user.getTenantId();
    Integer userId = user.getId();
    // Retrieve and return settings from DB using the userId and possibly the tenantId    
}

Depending on how you are doing multi-tenancy in your DB you may need to provide both tenantID and userID in your queries (to make sure things are safely retrieved). If you have separate DB per tenant then you might not need that information within your endpoints.

发布评论

评论列表(0)

  1. 暂无评论