Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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!

like image 499
wjsgzcn Avatar asked Apr 10 '19 09:04

wjsgzcn


2 Answers

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;
    }

}
like image 162
yosel vera Avatar answered Sep 30 '22 20:09

yosel vera


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());
    }
like image 23
Jose Mhlanga Avatar answered Sep 30 '22 19:09

Jose Mhlanga