How to extend OAuth2 principal

 ̄綄美尐妖づ 提交于 2021-02-08 04:27:47

问题


We are developing an application that uses OAuth 2 for two use cases:

  1. Access to backend microservies (using client_credentials)
  2. Authenticating the application's users (using authorization_code, so redirecting the users to Keycloak for login, roughly configured like shown in the tutorial).

While authenticating our users, we receive part of the information from the auth server (such as login) and the other part can be found in a local user table. What we like to do is to create a Principal object containing also the data from the local database.

PrincipalExtractor seems to be the way to go. Since we have to use manual OAuth configuration to not interfere with OAuth use case 1, we create it and set it:

tokenServices.setPrincipalExtractor(ourPrincipalExtractor);

The implementation basically does a DB lookup and returns a CustomUser object in the mapping function. Now although this seems to work (extractor is called), it is not persisted in the session correctly. So in many of our REST resource we are injecting the current user:

someRequestHandler(@AuthenticationPrincipal CustomUser activeUser) {

and receive null there. Looking into an injected Authentication it shows that it is an OAuth2Authentication object with the default Principal object (I think it is a Spring User / UserDetails). So null because it is not our CustomUser returned before.

Have we misunderstood the way PrincipalExtractor works? Can it be a misconfiguration of our filter chain because we have two different OAuth mechanisms in the same application as mentioned before? A breakpoint in Spring's Principal repository showed us that CustomUser is saved there, followed by a save with the original type which seems to overwrite it.


回答1:


I can tell you how I managed to do something similar using JWT. If you aren't using JWT then I'm not sure if this will help.

I had a very similar issue in that my injected principal was only containing the username. Not null like yours, but obviously not what I wanted. What I ended up doing was extending both the TokenEnhancer and JwtAccessTokenConverter.


I use the TokenEnhancer to embed my extended principal of type CustomUserDetailsinside the JWT additional information.

public class CustomAccessTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Authentication userAuthentication = authentication.getUserAuthentication();
        if (userAuthentication != null) {
            Object principal = authentication.getUserAuthentication().getPrincipal();
            if (principal instanceof CustomUserDetails) {
                Map<String, Object> additionalInfo = new HashMap<>();
                additionalInfo.put("userDetails", principal);
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
            }
        }
        return accessToken;
    }
}


And then manually extract the extended principal when building the Authentication object when processing an authenticated request.

public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        OAuth2Authentication authentication = super.extractAuthentication(map);
        Authentication userAuthentication = authentication.getUserAuthentication();

        if (userAuthentication != null) {
            LinkedHashMap userDetails = (LinkedHashMap) map.get("userDetails");
            if (userDetails != null) {

                // build your extended principal here
                String localUserTableField = (String) userDetails.get("localUserTableField");
                CustomUserDetails extendedPrincipal = new CustomUserDetails(localUserTableField);

                Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();

                userAuthentication = new UsernamePasswordAuthenticationToken(extendedPrincipal,
                        userAuthentication.getCredentials(), authorities);
            }
        }
        return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
    }
}


and the AuthorizationServer configuration to tie it all together.

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private DataSource dataSource;

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        CustomJwtAccessTokenConverter accessTokenConverter = new CustomJwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("a1b2c3d4e5f6g");
        return accessTokenConverter;
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new CustomAccessTokenEnhancer();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
        endpoints
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.passwordEncoder(passwordEncoder());
        security.checkTokenAccess("isAuthenticated()");
    }
}


I am then able to access my extended principal in my resource controller like this

@RestController
public class SomeResourceController {

    @RequestMapping("/some-resource")
    public ResponseEntity<?> someResource(Authentication authentication) {
        CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
        return ResponseEntity.ok("woo hoo!");
    }

}



回答2:


Ok, to answer my own question:

  1. PrincipalExtractor seems to be the usual and standard way to customize the principal
  2. It doesn't work in our case because we are using a JHipster application that simply overwrites the principal right after the login with it's own User. So all mapping in PrincipalExtractor is reset. If anyone has the same question: Look into UserService.

That's the downside of using generated code you don't know in detail I guess.



来源:https://stackoverflow.com/questions/48708406/how-to-extend-oauth2-principal

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!