How to set user authorities from user claims return by an oauth server in spring security
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
If you print the attributes returned by the
Oauth2UserAuthirty class
you will soon notice the contents of theJSON
data does not have therole
key instead has anauthorities
key hence you need to use that key to iterate over the list of authorities (roles) to get the actual role name.Hence the following lines of code will not work as there is no
role
key in theJSON
data returned by theoauth2UserAuthority.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());
}
wjsgzcn
Updated on September 15, 2022Comments
-
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 almost 5 yearsYour answer seems to be right! I will test it later and THANK YOU!
-
wjsgzcn almost 5 yearsI'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 almost 5 yearsYou 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 almost 5 yearsthe 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 almost 5 yearsI've resovled the problem, and see the edit of my post.