简体   繁体   中英

How to get current user authentication inside UserDetailsService

In my application I am trying to unite ActiveDirectory authentication with OAuth2 refresh tokens.

I was able to successfully authenticate via ActiveDirectoryLdapAuthenticationProvider . I have also provided my custom implementation of LdapUserDetailsMapper that populates the UserDetails with some custom attributes taken from ActiveDirectory . Key thing here is that these attributes have a confidentialty flag set on them and are only available to the user itself (ie authenticated user could read the values of these attributes for himself but not for the others). These attributes are stored in Authentication object and are used by an application in a context of an authenticated user.

Things get tricky when I try to add refresh tokens to the picture. Refresh tokens require me to implement a UserDetailsService where I have to provide new UserDetails having just a user name. This is not feasible due to confidentialty flag. Even if I have some master account in my application with the ability to browse ActiveDirectory I will not be able to retrieve the confidential attributes.

So I would rather prefer to provide more atomic implementations like the function that checks if the user is still active or the function that provides a renewed set of user authorities. Unfortunately I did not find this level of atomicity in Spring Security . So it looks like for refresh tokens I have to provide an implementation of UserDetailsService .

If I have to provide new user details I would like to have an access to previous user Authentication object. In this case I will check the user and if it is still active I will copy all the confidential information from previous Authentication . The problem is that it does not seem to be available. At the moment when UserDetailsService::loadUserByUsername() is called SecurityContextHolder.getContext() does not contain the user authentication. Authentication is also not available from UserDetailsService API - I only get the user name. At the same time user's Authentication object is present just one stack frame up in UserDetailsByNameServiceWrapper class:

public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException { return this.userDetailsService.loadUserByUsername(authentication.getName()); }

The least thing I want to do here is to implement some in-memory storage for all user confidential information to be used whenever I need to provide new UserDetails . I already have all the required information in user authentication managed by Spring and doing this on my end seems to be just surplus.

And here comes question list:

  1. If you feel that I am doing something terribly wrong from the perspective of application security architecture, please tell me
  2. Is there a way to tell Spring during refresh token procedure to use previous UserDetails object so that application could just answer the question if the user is still active and should be issued a new access token (and not provide the UserDetailsService at all)?
  3. Is there a way to get previous user Authentication object during the call to UserDetailsService::loadUserByUsername() so that I could use it as a source of confidential info?
  4. Is there some other approach that I do not see at the moment to add refresh tokens to my application?

Update:

Here I saw a comment that you could implement your own AuthenticationUserDetailsService to work around the problem. This I do not see how to do. It is hardcoded in AuthorizationServerEndpointsConfigurer that it always creates an instance of UserDetailsByNameServiceWrapper so to provide your own implementation you would have to interfere into AuthorizationServerEndpointsConfigurer initialization process.

OK, looks like the answer with Spring Security 4.0 is you can't .

So I had to apply the following hack which works, but I do not like it very much. Since it works I am posting it here. Since it does not solve the original problem, but works around it I will not mark it as accepted by the author.

  1. Switch to JWT tokens
  2. Use custom TokenEnhancer to inject all information that is required to recreate the user (user secret in my case) to the token directly. Of course, the value must be encrypted by the server with symmetrical crypto algorithm before adding it to the token.
  3. Instruct authorization server to use custom AccessTokenConverter . This implementation of AccessTokenConverter would extract the secret value from the token, decrypt it and put it to ThreadLocal field.
  4. Instruct custom UserDetailsService to retrieve the user secret from the ThreadLocal field set in step 3 . This is the best way I found so far to deliver the current authorization context to UserDetailsService . And this is the part that I do not like most in my solution.
  5. Use custom security filter to erase the value set in step 3 from ThreadLocal field.

PS I still do not see the possibility to implement custom AuthenticationUserDetailsService that was mentioned earlier. If such possibility exists it could have been another way to solve the problem.

Some useful links:

I've got the response from Joe Grandja on spring-security-oauth github page .

Posting it here since it actually provides an answer to the original question.

Hi @masm22. To help with question 1 and 2, below is a custom configuration that will allow you to hook into the refresh_token grant and provide your own behaviour or delegate to super to proceed with current behaviour. It will also allow you to access the user Authentication so you can read your custom (confidential) attributes.

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

        .....   // other config

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenServices(this.customTokenServices());
    }

    private DefaultTokenServices customTokenServices() {
        DefaultTokenServices tokenServices = new CustomTokenServices();
        tokenServices.setTokenStore(new InMemoryTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(true);
        tokenServices.setClientDetailsService(this.clientDetailsService);
        return tokenServices;
    }

    private static class CustomTokenServices extends DefaultTokenServices {
        private TokenStore tokenStore;

        @Override
        public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
            OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
            OAuth2Authentication authentication = this.tokenStore.readAuthenticationForRefreshToken(refreshToken);

            // Check attributes in the authentication and
            // decide whether to grant the refresh token
            boolean allowRefresh = true;

            if (!allowRefresh) {
                // throw UnauthorizedClientException or something similar

            }

            return super.refreshAccessToken(refreshTokenValue, tokenRequest);
        }

        @Override
        public void setTokenStore(TokenStore tokenStore) {
            super.setTokenStore(tokenStore);
            this.tokenStore = tokenStore;
        }
    }
}

The other thing I want to point out for your information is in DefaultTokenServices.refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) has the following code:

    OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
    if (this.authenticationManager != null && !authentication.isClientOnly()) {
        // The client has already been authenticated, but the user authentication might be old now, so give it a
        // chance to re-authenticate.
        Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
        user = authenticationManager.authenticate(user);
        Object details = authentication.getDetails();
        authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
        authentication.setDetails(details);
    }

The user is being re-authenticated. Possibly something you may want to do in your custom implementation if need be.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM