Expected Behavior
When using oauth2login it is should be possible to use the access token in GrantedAuthoritiesMapper in order to map access token claims to authorities for use in hasRole/hasAuthority. This is espacially usefull when some claims do only appear in the access token.
Current Behavior GrantedAuthoritiesMapper has currently no access to the accessToken.
Context https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html#webflux-oauth2-login-advanced-map-authorities-grantedauthoritiesmapper
My (dirty) workaround involves extending the OidcUserService and returning a new OidcUser Object.
Comment From: jzheaux
Hi, @Xyaren, thanks for reaching out. What is making you feel like extending OidcUserService is a dirty workaround? I'm thinking you might do something like this:
@Component
public class MyOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final OidcUserService delegate = new OidcUserService();
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser user = this.delegate.loadUser(userRequest);
Collection<GrantedAuthority> authorities = new ArrayList<>(user.getAuthorities());
// ... add custom authorities
return new DefaultOidcUser(authorities, user.getIdToken(), user.getUserInfo());
}
}
which doesn't seem dirty to me. I want to make sure I'm not missing something; can you clarify please?
Comment From: Xyaren
Hi, thanks for the quick response! I do have a similar workaround, that I'll attach at the end.
It feels odd wrapping the original service, tear apart the response and stitch it together again in a new instance of the same object. I also was unsure about "reconstructing" the OidcUser due to no access to nameAttributeKey which is required for the all-arg constructor. Therefore I went for extending the default implementation and wrapping the result. It there anything speaking against having the Access token available within the OidcUser ? This would allow users to use a GrantedAuthoritiesMapper to map from any of the sources.
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import packages.from.my.company.that.i.dont.want.to.name.JwtToAuthorityExtractor;
import lombok.AllArgsConstructor;
import lombok.experimental.Delegate;
import org.jetbrains.annotations.NotNull;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
@AllArgsConstructor
public class ExtendedOidcUserService extends OidcUserService {
private SupplierJwtDecoder supplierJwtDecoder;
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
var oidcUser = super.loadUser(userRequest);
var accessToken = userRequest.getAccessToken();
var authorityExtractor = new JwtToAuthorityExtractor(
userRequest.getClientRegistration().getClientId(),
Set.of(),
false);
var jwt = supplierJwtDecoder.decode(accessToken.getTokenValue());
Set<GrantedAuthority> additionalAuthorities = authorityExtractor.authorities(jwt).collect(Collectors.toSet());
return new ExtendedOidcUser(oidcUser, additionalAuthorities);
}
static class ExtendedOidcUser implements OidcUser {
@Delegate
private final OidcUser oidcUser;
private final Collection<? extends GrantedAuthority> authorities;
public ExtendedOidcUser(OidcUser oidcUser, Collection<? extends GrantedAuthority> additionalAuthorities) {
this.oidcUser = oidcUser;
this.authorities = joinAuthorities(oidcUser, additionalAuthorities);
}
@SuppressWarnings({"LombokGetterMayBeUsed", "RedundantSuppression"})
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@NotNull
private static HashSet<GrantedAuthority> joinAuthorities(
OidcUser oidcUser,
Collection<? extends GrantedAuthority> additionalAuthorities) {
HashSet<GrantedAuthority> totalAuthorities = new HashSet<>(
oidcUser.getAuthorities().size() + additionalAuthorities.size());
totalAuthorities.addAll(oidcUser.getAuthorities());
totalAuthorities.addAll(additionalAuthorities);
return totalAuthorities;
}
}
}
Comment From: jzheaux
Thanks for reporting this, @Xyaren, and sorry for the delay. setOidcUserMapper was added to OidcUserService in 6.3.
You should now instead be able to do the following:
@Bean
OidcUserService oidcUserService() {
OidcUserService service = new OidcUserService();
service.setOidcUserMapper((request, userInfo) -> {
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Collection<GrantedAuthority> authorities = authorityExtractor.authorities().collect(Collectors.toSet());
String userNameAttributeName = "preferred_username";
return new DefaultOidcUser(authorities, userRequest.getIdToken(),
userInfo, userNameAttributeName);
});
return service;
}
Does this allow you to remove your wrapper class and service extension? If so, I think we'll close this issue and guide people to use that method to get the access token.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.