Spring-security-oauth: Client ID getting passed as username on the authentication

Created on 19 Mar 2015  路  33Comments  路  Source: spring-projects/spring-security-oauth

I am trying to generate a token with grant_type password and when I get to userdetails service , my client id is getting passed as user name . I am using custom client details service and user details service , all in one class. I am pretty sure I am doing some wrong . My client details and user details are retrieved from mongo.

Auth server config

    private @Autowired OAuthUserService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService);

    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // @formatter:off
        clients.withClientDetails(clientDetailsService);
}

ClientDetailsUserDetailService

@Component
public class OAuthUserService implements ClientDetailsService,
        UserDetailsService {


    private @Autowired OAuthClientDetailsRepository clientDetailsDao;

    private @Autowired OAuthUserRepository userDetailsDao;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException 
    {
        OAuthUser user=userDetailsDao.findByLogin(username);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
        }
        return new UserRepositoryUserDetails(user);
    }

    private final static class UserRepositoryUserDetails extends OAuthUser implements UserDetails
    {

        /**
         * 
         */
        private static final long serialVersionUID = -4024636858150373536L;

        private UserRepositoryUserDetails() {

        }

        private UserRepositoryUserDetails(OAuthUser user) {
            super(user);
        }

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {

            return AuthorityUtils.commaSeparatedStringToAuthorityList(getRoles());
        }

        @Override
        public String getUsername() {

            return getLogin();
        }

        @Override
        public boolean isAccountNonExpired() {

            return isEnabled();
        }

        @Override
        public boolean isAccountNonLocked() {

            return isEnabled();
        }

        @Override
        public boolean isCredentialsNonExpired() {

            return isEnabled();
        }

        @Override
        public boolean isEnabled() {

            return true;
        }

    }

    @Override
    public ClientDetails loadClientByClientId(String clientId)
            throws ClientRegistrationException 
    {
        OAuthClientDetails oauthClientDetails= clientDetailsDao.findByClientId(clientId);
        if (oauthClientDetails == null) {
            throw new UsernameNotFoundException(String.format("ClientDetails %s does not exist!", clientId));
        }
        BaseClientDetails clientDetails=new BaseClientDetails();

        clientDetails.setClientId(oauthClientDetails.getClientId());
        clientDetails.setScope(StringUtils.commaDelimitedListToSet(oauthClientDetails.getScope()));
        clientDetails.setAuthorizedGrantTypes(StringUtils.commaDelimitedListToSet(oauthClientDetails.getAuthorizedGrantTypes()));
        clientDetails.setAuthorities(AuthorityUtils.createAuthorityList(StringUtils.commaDelimitedListToStringArray(oauthClientDetails.getAuthorities())));
        clientDetails.setAccessTokenValiditySeconds(oauthClientDetails.getAccessTokenValidity());
        clientDetails.setClientSecret(oauthClientDetails.getClientSecret());
        clientDetails.setAdditionalInformation(oauthClientDetails.getAdditionalInformation());
        if(oauthClientDetails.isAutoApprove())
            clientDetails.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(oauthClientDetails.getAutoApproveScopes()));
        else
            clientDetails.setAutoApproveScopes(new HashSet<String>());

        clientDetails.setAccessTokenValiditySeconds(oauthClientDetails.getAccessTokenValidity());
        clientDetails.setRegisteredRedirectUri(StringUtils.commaDelimitedListToSet(oauthClientDetails.getWebserverRedirectURL()));
        clientDetails.setResourceIds(StringUtils.commaDelimitedListToSet(oauthClientDetails.getResourceIds()));
        clientDetails.setAdditionalInformation(oauthClientDetails.getAdditionalInformation());

        return clientDetails;
    }
stackoverflow

Most helpful comment

hey @mfalzetta .
Finally, i found a solution in StackOverFlow:
http://stackoverflow.com/questions/26208087/spring-boot-oauth-2-0-userdetails-user-not-found

All 33 comments

implements ClientDetailsService, UserDetailsService

Why do you feel the need to do that?

The /token endpoint is protected by Basic auth (by default, and according to the recommendations in the spec) using client_id and client_secret as the credentials. So you should expect to see client_id as a "username" (an AuthentcationManager is provided by the framework so in most cases you only have to provide a ClientDetailsService). I don't see why you have to be able to load users as well in the same service.

if my grant_type =password , for example my curl looks some like this curl -X POST -vu passwordclient:secret http://localhost:9000/oauth/token -d "grant_type=password&username=user&password=test", if I need to authenticate my user and password from a database , wont I need userdeatails service as well . I am pretty sure I am missing some really stupid here .

The client is authenticated from the header (-u in your curl command) using the ClientDetailsService. The user is authenticated from the request body using whatever AuthenticationManager you passed into the AuthorizationServerConfigurer (I don't see that method in your code snippet above, so maybe that's what you are missing).

Thats what I thought and its puzzling me , here is Authserverconfig. I am using custom token store with mongoimpl , I dont think that matters for this., just an FYI.

@Configuration
@EnableAuthorizationServer
@EnableWebMvcSecurity
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter{




    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    private @Autowired  OAuthClientDetailsService clientDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
            throws Exception {
        // @formatter:off
        endpoints
            .tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer())
            .authenticationManager(this.authenticationManager).userApprovalHandler(approvalHandler());
        // @formatter:on
    }

       @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {

            oauthServer.tokenKeyAccess("isAnonymous()").checkTokenAccess("permitAll()");
        }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // @formatter:off
        clients.withClientDetails(clientDetailsService);


        // @formatter:on
    }



       @Bean
        @Qualifier("tokenStore")
        public TokenStore tokenStore()
        {

            System.out.println("Created JwtTokenStore");
            return new JWTMongoTokenStore(jwtTokenEnhancer());
        }




     @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwtkeyaeb.jks"), "assurant".toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwtkeyaeb"));
        return converter;
    }

     @Bean 
        public UserApprovalHandler approvalHandler()
        {
            UserApprovalHandler approvalHandler=new DefaultUserApprovalHandler();
            return approvalHandler;

        }

My websecurity config

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {



    private @Autowired   OAuthUserService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService);

    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

So what is it that doesn't work?

user not getting authenticated from request body , I am not seeing my username passed in rather I am seeing clientId's name as username during the invocation of loadUserByUsername call.

But that's expected since that is what is being authenticated. Show us the stack (e.g. from a debugger) when the loadUserByUsername is called.

screen shot 2015-03-19 at 9 23 14 am

Right so that's in the BasicAuthenticationFilter authenticating the client. Completely as it should be.

excuse my ignorance , is there a way to get around it ?

Get around what? It's doing what it it is supposed to.

Sorry I mis-understood , I was assuming that you were implying me to do something else.

This might be something you might have mentioned and it probably didnt click on , when I switched the clientdetailsservice configurer from clientdetails service to jdbc or inmemory , my client request is getting authenticated, when I use my custom client details its not.

Modified config

@Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception 


    {
        // @formatter:off
        clients
        //.withClientDetails(clientDetailsService)
        .inMemory()
        .withClient("passwordclient")
                    .authorizedGrantTypes("password", "refresh_token")
                    .authorities("ROLE_USER")
                    .scopes("read", "write")
                    .resourceIds("hello:read,time:read,hello:write,time:write")
                    .accessTokenValiditySeconds(0)
                    .refreshTokenValiditySeconds(0)
                    .autoApprove(false)
                    .autoApprove("trust")
                    .additionalInformation(new LinkedHashMap<String, List<String>>())
                    .secret("secret");

        // @formatter:on
    }

I also encountered this issue, any development on the fix?

Did you follow the discussion? I'm pretty sure this is user error (misconfiguration).

The confusing part here is the OAuth2AuthenticationManager uses ClientDetailsService to authenticate the user. When grant_type is "password", it would be more intuitive if it used UserDetailsService.

Sorry for the confusion. The OAuth2AuthenticationManager never authenticates clients. Neither does it play any role in the password grant. It is used in a Resource Server. Is that clearer?

Yes and no. I made a bad assumption about the @Autowired AuthenticationManager in AuthorizationServerConfigurerAdapter. Stepping through it, i now see it's a ProviderManager.

My problem is that I want to use my own UserDetailsService to get the user (not the client) when authenticating a grant_type=password token. I expected the code below to work, but it doesn't.

public void configure(AuthorizationServerEndpointsConfigurer oauthServer) throws Exception {
    oauthServer
        .authenticationManager(authenticationManager)
        .tokenStore(tokenStore)
        .userDetailsService(myUserDetailsService)  // This is not used to authenticate the user
        .setClientDetailsService(clientDetailsService);  
}

Any conclusion here? Also I want "ClientDetailsServiceConfigurer" with My client details is retrieved from MySQL database.

Only way I got it working was to doing this.

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception
{
// @formatter:off
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer())
.userApprovalHandler(approvalHandler())
.authenticationManager(authenticationManager);
// @formatter:on
}

@nandhusriram Could you give me more code example of elaobration?

There's a JDBC sample in the integration tests. What's different about your app?

@dsyer Any example code of combined the OAuthUser with role of resourceId mapping in MySQL database?

What do you mean? Is there something you do that isn't covered in the integration test?

Yes, I am asking how to map the ResourceServer related resourceId to role assigned user or clientId?

I don't think that's anything to do with JDBC or about authentication. If you want to ask a general question on features or usage then Stackoverfow is probably the best place.

@derikl @dsyer
So please correct me if I'm wrong, but if the below example mentioned in the discusion is NOT setting the custom UserDetailsService used in grant_type=password, how could I achieve that then? Thank you in advance :)

public void configure(AuthorizationServerEndpointsConfigurer oauthServer) throws Exception {
oauthServer
.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
.userDetailsService(myUserDetailsService) // This is not used to authenticate the user
.setClientDetailsService(clientDetailsService);
}

The authentication manager is used for authentication. You can create a custom one with your user details service using the normal Spring Security builder APIs (per the core docs and dozens of samples).

Yes, like @dsyer said, you need to create your own AuthenticationManager. I did this.

   public void configure(AuthorizationServerEndpointsConfigurer oauthServer) throws Exception {
      MyAuthenticationManager myAuthManager = new MyAuthenticationManager(userDetailsService);
      oauthServer
         .authenticationManager(myAuthManager)
         .tokenStore(tokenStore)
         .userDetailsService(userDetailsService)  // This is not used to authenticate the user, unexpected...
         .setClientDetailsService(clientDetailsService);  
   }

In your AuthenticationManager, override
Authentication authenticate(Authentication authentication) throws AuthenticationException;

If you want to get information about the scope or grant_type, etc., It's in authentication.getDetails().

Hi @nandhusriram

I have the similar problem.

In my implementation i need validade the username and password of my application in my database but i see that spring use client id and secret and obviously not not exist in my database.

If i send user name and password in header (and not client id and secret) the authentication is ok but a error "Handling error: NoSuchClientException, No client with requested id: pai' occur.

You solve your problem? Can help me with a tip?

Regards.

hey @mfalzetta .
Finally, i found a solution in StackOverFlow:
http://stackoverflow.com/questions/26208087/spring-boot-oauth-2-0-userdetails-user-not-found

I found that if I made the AuthenticationManager a bean and then passed it to the OAuth2, and I sent invalid client credentials, it will use my AuthenticationManager to try to validate my client credentials (and I only intended this AuthenticationManager to be used for user authentication (in a token grant). I fixed this by not defining the AuthenticationManager as a bean (meaning I've gotta use normal Java constructors not @Autowired or anything) and constructing the AuthenticationManager object in the configure override of my AuthorizationServerConfigurerAdapter as follows:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // NOTE: WESAuthenticationManager should not be a Bean so that the default Spring Security can apply for other end-points (and use Basic authentication with the client details)
    endpoints.authenticationManager(new WESAuthenticationManager(authFactory, securityManager));
    endpoints.setClientDetailsService(clientDetailsService);
    endpoints.tokenStore(tokenStore());
    endpoints.accessTokenConverter(accessTokenConverter());
    endpoints.tokenEnhancer(accessTokenConverter());
    // this is used by the token refresh to get user details to verify if the account is still active
    endpoints.userDetailsService(userDetailsService());
}

In other words, like @derikl said...

Closing this as questions are better suited on Stack Overflow. We prefer to use GitHub issues for bugs and enhancements.

Was this page helpful?
0 / 5 - 0 ratings