How to set user authorities from user claims return by an oauth server in spring security

13,975

Solution 1

I spent a few hours and I find the solution. The problem is with spring oauth security, by default it obtain the user roles from the token using the key 'authorities'. So, I implemented a custom token converter.

The first you need is the custom user token converter, here is the class:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

public class CustomUserTokenConverter implements UserAuthenticationConverter {
    private Collection<? extends GrantedAuthority> defaultAuthorities;
    private UserDetailsService userDetailsService;

    private final String AUTHORITIES = "role";
    private final String USERNAME = "preferred_username";
    private final String USER_IDENTIFIER = "sub";

    public CustomUserTokenConverter() {
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public void setDefaultAuthorities(String[] defaultAuthorities) {
        this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(defaultAuthorities));
    }

    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        Map<String, Object> response = new LinkedHashMap();
        response.put(USERNAME, authentication.getName());
        if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
            response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        }

        return response;
    }

    public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(USER_IDENTIFIER)) {
            Object principal = map.get(USER_IDENTIFIER);
            Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map);
            if (this.userDetailsService != null) {
                UserDetails user = this.userDetailsService.loadUserByUsername((String)map.get(USER_IDENTIFIER));
                authorities = user.getAuthorities();
                principal = user;
            }

            return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
        } else {
            return null;
        }
    }

    private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
        if (!map.containsKey(AUTHORITIES)) {
            return this.defaultAuthorities;
        } else {
            Object authorities = map.get(AUTHORITIES);
            if (authorities instanceof String) {
                return AuthorityUtils.commaSeparatedStringToAuthorityList((String)authorities);
            } else if (authorities instanceof Collection) {
                return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection)authorities));
            } else {
                throw new IllegalArgumentException("Authorities must be either a String or a Collection");
            }
        }
    }
}

The you need a custom token converter, here is:

import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
        OAuth2Authentication authentication = super.extractAuthentication(claims);
        authentication.setDetails(claims);
        return authentication;
    }


}

And finally you ResourceServerConfiguration looks like this:

import hello.helper.CustomAccessTokenConverter;
import hello.helper.CustomUserTokenConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(final HttpSecurity http) throws Exception {
        // @formatter:off
        http.authorizeRequests()
                .anyRequest().access("hasAnyAuthority('Admin')");
    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("arawaks");
    }

    @Bean
    @Primary
    public RemoteTokenServices tokenServices() {
        final RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId("resourceId");
        tokenServices.setClientSecret("resource.secret");
        tokenServices.setCheckTokenEndpointUrl("http://localhost:5001/connect/introspect");
        tokenServices.setAccessTokenConverter(accessTokenConverter());
        return tokenServices;
    }


    @Bean
    public CustomAccessTokenConverter accessTokenConverter() {
        final CustomAccessTokenConverter converter = new CustomAccessTokenConverter();
        converter.setUserTokenConverter(new CustomUserTokenConverter());
        return converter;
    }

}

Solution 2

Apparently @wjsgzcn answer (EDIT 1) DOES NOT WORK for reasons below

  1. If you print the attributes returned by the Oauth2UserAuthirty class you will soon notice the contents of the JSON data does not have the role key instead has an authorities key hence you need to use that key to iterate over the list of authorities (roles) to get the actual role name.

  2. Hence the following lines of code will not work as there is no role key in the JSON data returned by the oauth2UserAuthority.getAttributes();

     OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
     Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
     if (userAttributes.containsKey("role")){
         String roleName = "ROLE_" + (String)userAttributes.get("role");
         mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
     }
    

So instead use the following to get the actual role from the getAttributes

if (userAttributes.containsKey("authorities")){
   ObjectMapper objectMapper = new ObjectMapper();
   ArrayList<Role> authorityList = 
   objectMapper.convertValue(userAttributes.get("authorities"), new 
   TypeReference<ArrayList<Role>>() {});
   log.info("authList: {}", authorityList);
   for(Role role: authorityList){
      String roleName = "ROLE_" + role.getAuthority();
      log.info("role: {}", roleName);
      mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
   }
}

Where the Role is a pojo class like so

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    @JsonProperty
    private String authority;
}

That way you will be able to get the ROLE_ post prefix which is the actual role granted to the user after successfully authenticated to the Authorization server and the client is returned the LIST of granted authorities (roles).

Now the complete GrantedAuthoritesMapper look like the following:

private GrantedAuthoritiesMapper userAuthoritiesMapper() {
    return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
                    
                    // Map the claims found in idToken and/or userInfo
                    // to one or more GrantedAuthority's and add it to mappedAuthorities
                    if (userInfo.containsClaim("authorities")){
                        ObjectMapper objectMapper = new ObjectMapper();
                        ArrayList<Role> authorityList = objectMapper.convertValue(userInfo.getClaimAsMap("authorities"), new TypeReference<ArrayList<Role>>() {});
                        log.info("authList: {}", authorityList);
                        for(Role role: authorityList){
                            String roleName = "ROLE_" + role.getAuthority();
                            log.info("role: {}", roleName);
                            mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                        } 
                    }

                } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                    OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
                    Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
                    log.info("userAttributes: {}", userAttributes);
                    // Map the attributes found in userAttributes
                    // to one or more GrantedAuthority's and add it to mappedAuthorities
                    if (userAttributes.containsKey("authorities")){
                        ObjectMapper objectMapper = new ObjectMapper();
                        ArrayList<Role> authorityList = objectMapper.convertValue(userAttributes.get("authorities"), new TypeReference<ArrayList<Role>>() {});
                        log.info("authList: {}", authorityList);
                        for(Role role: authorityList){
                            String roleName = "ROLE_" + role.getAuthority();
                            log.info("role: {}", roleName);
                            mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                        } 
                    }
                }
            });
            log.info("The user authorities: {}", mappedAuthorities);
            return mappedAuthorities;
    };
}

Now you are able to use the userAuthorityMapper in your oauth2Login as follows

@Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**").authorizeRequests()
            .antMatchers("/", "/login**").permitAll()
            .antMatchers("/clientPage/**").hasRole("CLIENT")
            .anyRequest().authenticated()
            .and()
            .oauth2Login()
                .userInfoEndpoint()
                .userAuthoritiesMapper(userAuthoritiesMapper());
    }
Share:
13,975
wjsgzcn
Author by

wjsgzcn

Updated on September 15, 2022

Comments

  • wjsgzcn
    wjsgzcn over 1 year

    I recently wrote a spring boot project that uses spring security oauth2, the auth server is IdentityServer4 for some reason, I can successfully login and get username in my project but I cannot find any way to set user's authority/role.

    request.isUserInRole always return false. @PreAuthorize("hasRole('rolename')") always lead me to 403.

    Where can I place some code to set the authorities?

    The server has returned some user claims through userinfo endpoint, and my project received them, and I can even see it in the principle param of my controller.

    This method always return 403

    @ResponseBody
    @RequestMapping("admin")
    @PreAuthorize("hasRole('admin')")
    public String admin(HttpServletRequest request){
        return "welcome, you are admin!" + request.isUserInRole("ROLE_admin");
    }
    

    application.properties

    spring.security.oauth2.client.provider.test.issuer-uri = http://localhost:5000
    spring.security.oauth2.client.provider.test.user-name-attribute = name
    
    spring.security.oauth2.client.registration.test.client-id = java
    spring.security.oauth2.client.registration.test.client-secret = secret
    spring.security.oauth2.client.registration.test.authorization-grant-type = authorization_code
    spring.security.oauth2.client.registration.test.scope = openid profile
    

    I print the claims

    @ResponseBody
    @RequestMapping()
    public Object index(Principal user){
        OAuth2AuthenticationToken token = (OAuth2AuthenticationToken)user;
        return token.getPrincipal().getAttributes();
    }
    

    and get the result show there is a claim named 'role'

    {"key":"value","role":"admin","preferred_username":"bob"}
    

    Anybody can help me and give me a solution please?

    EDIT 1: The reason is oauth2 client has removed the extracter, and I have to implement the userAuthoritiesMapper.

    Finally I got this work by adding the following class:

    @Configuration
    public class AppConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper());
            //.oidcUserService(this.oidcUserService());
            super.configure(http);
        }
    
        private GrantedAuthoritiesMapper userAuthoritiesMapper() {
            return (authorities) -> {
                Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    
                authorities.forEach(authority -> {
                    if (OidcUserAuthority.class.isInstance(authority)) {
                        OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
    
                        OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
                        if (userInfo.containsClaim("role")){
                            String roleName = "ROLE_" + userInfo.getClaimAsString("role");
                            mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                        }
                    } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                        OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
                        Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
                        
                        if (userAttributes.containsKey("role")){
                            String roleName = "ROLE_" + (String)userAttributes.get("role");
                            mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                        }
                    }
                });
    
                return mappedAuthorities;
            };
        }
    }
    

    The framework changes so fast and the demos on the web is too old!

  • wjsgzcn
    wjsgzcn almost 5 years
    Your answer seems to be right! I will test it later and THANK YOU!
  • wjsgzcn
    wjsgzcn almost 5 years
    I'm trying to resolve it by adding a claim named "authorities" with value = ["ROLE_admin"], but spring security still return 403, the token is :"preferred_username":"bob","authorities":"[\"ROLE_admin\"]"
  • yosel vera
    yosel vera almost 5 years
    You don't need to add the prefix ROLE_ to your roles, my first solution was something like that, in the IdentityServer4, at the ProfileService implementation, I added the claim "authorities" and it works without change the default TokenConverter of spring security oauth2 and with the same role names and without the prefix ROLE_.
  • wjsgzcn
    wjsgzcn almost 5 years
    the authorities claim seemed to be ignored, I have tried many formats of the claim value with no luck. and I use spring-boot-starter-oauth2-client package, there is no UserAuthenticationConverter class.
  • wjsgzcn
    wjsgzcn almost 5 years
    I've resovled the problem, and see the edit of my post.