Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I validate OAuth 2.0 token user details in @PreAuthorize annotation in Spring Boot REST service

I need to make a check in @PreAuthorize annotation. Something like:

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")

That is OK but I also need to validate some user details stored in the OAuth 2.0 token with those in the request path so I would need to do something like (oauthToken.userDetails is just an example:

@PreAuthorize("#pathProfileId.equals(oauthToken.userDetails.profileId)")

(profileId is not userId or userName, it is a user details that we add in the OAuth token when we create it)

What is the simplest way to make OAuth token properties visible in the preauthorized annotation security expression language?

like image 553
icordoba Avatar asked Oct 27 '22 11:10

icordoba


1 Answers

You have two options:

1-

Setting UserDetailsService instance into DefaultUserAuthenticationConverter and set converter to JwtAccessTokenConverter so when spring calls extractAuthentication method from DefaultUserAuthenticationConverter it found (userDetailsService != null) so it get the whole UserDetails object by calling implementation of loadUserByUsername when calling this line:

userDetailsService.loadUserByUsername((String) map.get(USERNAME))

implemented in next method inside spring class org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter.java but just adding it to clarify how spring get principal object from map (first getting it by username, and if userDetailsService not null so it get the whole object):

//Note: This method implemented by spring but just putting it to show where spring exctract principal object and how extracting it
public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(USERNAME)) {
            Object principal = map.get(USERNAME);
            Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
            if (userDetailsService != null) {
                UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
                authorities = user.getAuthorities();
                principal = user;
            }
            return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
        }
        return null;
    }

So what you need to implement in your microservice is:

@Bean//this method just used with token store bean example: new JwtTokenStore(tokenEnhancer());
public JwtAccessTokenConverter tokenEnhancer() {
    /**
    * CustomTokenConverter is a class extends JwtAccessTokenConverter 
    * which override "enhance" to add extra information to OAuth2AccessToken after
    * authenticate the user and get it by loadUserByUsername implementation 
    * like profileId in your case
    **/  
    JwtAccessTokenConverter converter = new CustomTokenConverter();

    DefaultAccessTokenConverter datc = new DefaultAccessTokenConverter();
    datc.setUserTokenConverter(userAuthenticationConverter());
    converter.setAccessTokenConverter(datc);

    //Other method code implementation....
}

@Autowired
private UserDetailsService userDetailsService;

@Bean
public UserAuthenticationConverter userAuthenticationConverter() {
    DefaultUserAuthenticationConverter duac = new DefaultUserAuthenticationConverter();
    duac.setUserDetailsService(userDetailsService);
    return duac;
 }

Note: this first way will hit database in every request so it load user by username and get UserDetails object so it assign it to principal object inside authentication.


2-

If for any reason you see it's better to not hit database in each request and no problem about executing data needed like profileId from token passed in request.

Assuming you know that old authorities assigned to user when generating oauth2 token will always be in token till it goes invalid even after you change it in database for user who passes the token in request so user could call a method not allowed to him/her anymore after extracting token and it was allowed before extracting the token.

So this means if user authorities changed after generating the token, new authorities will not be checked by @PreAuthorize as it's not removed or added to token and you have to wait till old token goes invalid or expired so user forced to execute the service again to get new oauth token.

Anyway, in this second option you only need to override extractAuthentication method inside CustomTokenConverter class extends JwtAccessTokenConverter and forget about setting access token converter converter.setAccessTokenConverter from tokenEnhancer() method in first option, and here are the whole CustomTokenConverter you can use it for reading data from token and return principal object not just string username:

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

public class CustomTokenConverter extends JwtAccessTokenConverter {

    // This is the method you need to override to read data direct from token passed in request
    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        OAuth2Authentication authentication = super.extractAuthentication(map);

        Object userIdObj = map.get(AuthenticationUtils.USER_ID);
        UUID userId = userIdObj != null ? UUID.fromString(userIdObj.toString()) : null;
        Object profileIdObj = map.get(AuthenticationUtils.PROFILE_ID);
        UUID profileId = profileIdObj != null ? UUID.fromString(profileIdObj.toString()) : null;
        Object firstNameObj = map.get(AuthenticationUtils.FIRST_NAME);
        String firstName = firstNameObj != null ? String.valueOf(firstNameObj) : null;
        Object lastNameObj = map.get(AuthenticationUtils.LAST_NAME);
        String lastName = lastNameObj != null ? String.valueOf(lastNameObj) : null;

        JwtUser principal = new JwtUser(userId, profileId, authentication.getUserAuthentication().getName(), "N/A", authentication.getUserAuthentication().getAuthorities(), firstName, lastName);

        authentication = new OAuth2Authentication(authentication.getOAuth2Request(),
                new UsernamePasswordAuthenticationToken(principal, "N/A", authentication.getUserAuthentication().getAuthorities()));
        return authentication;
    }

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        JwtUser user = (JwtUser) authentication.getPrincipal();
        Map<String, Object> info = new LinkedHashMap<>(accessToken.getAdditionalInformation());
        if (user.getId() != null)
            info.put(AuthenticationUtils.USER_ID, user.getId());
        if (user.getProfileId() != null)
            info.put(AuthenticationUtils.PROFILE_ID, user.getProfileId());
        if (isNotNullNotEmpty(user.getFirstName()))
            info.put(AuthenticationUtils.FIRST_NAME, user.getFirstName());
        if (isNotNullNotEmpty(user.getLastName()))
            info.put(AuthenticationUtils.LAST_NAME, user.getLastName());

        DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
        customAccessToken.setAdditionalInformation(info);
        return super.enhance(customAccessToken, authentication);
    }

    private boolean isNotNullNotEmpty(String str) {
        return Optional.ofNullable(str).map(String::trim).map(string -> !str.isEmpty()).orElse(false);
    }

}

Finally: Guess how i know you are asking about JWT used with OAuth2?

Because i am a part of your company :P and you know that :P

like image 197
mibrahim.iti Avatar answered Nov 15 '22 10:11

mibrahim.iti