2

I am using Spring Security OAuth2 2.0.7.RELEASE. As i am using ORM to connect to my database and default JdbcUserDetailsManager uses jdbc i wanted to implement my own UserDetailsService, which is

@Service
public class UserService
    implements UserDetailsService {

    @Override
    public UserDetailsService loadUserByUsername(String username) throws UsernameNotFoundException {
        // I tested this logic and works fine so i avoid this lines
        return userDetailsService;
    }
}

Besides, i've modified authorities schema as follows:

mysql> describe authorities;
+--------------+---------------------+------+-----+---------+----------------+
| Field        | Type                | Null | Key | Default | Extra          |
+--------------+---------------------+------+-----+---------+----------------+
| authority_id | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| user_id      | bigint(20) unsigned | NO   | MUL | NULL    |                |
| authority    | varchar(256)        | NO   |     | NULL    |                |
+--------------+---------------------+------+-----+---------+----------------+

Then i am injecting my custom userDetailsService like this:

@Configuration
@Import(OAuth2SupportConfig.class)
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends
        AuthorizationServerConfigurerAdapter {

  ...    

  @Autowired
  private UserDetailsService userDetailsService

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer    endpoints)
            throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore).tokenServices(tokenService);
        endpoints.userDetailsService(userDetailsService); // Inject custom
        endpoints.authorizationCodeServices(authorizationCodeServices);
    }

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

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AuthenticationManagerConfiguration
    extends GlobalAuthenticationConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService userService;

    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception {
 auth.jdbcAuthentication().dataSource(this.dataSource).and().userDetailsService(this.userService);// Inject custom
    }
}

If i send /oauth/token request with grant_type=password then i get this error

POST /oauth/token HTTP/1.1
Host: localhost:8080
Authorization: Basic aW5kaXJhOnNlY3JldA==
Cache-Control: no-cache
Postman-Token: c89baf37-8ad2-4270-5251-9715bfab470a
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=user&password=pass

(where clientId and clientSecret is encoded)

{
  "error": "unauthorized",
  "error_description": "PreparedStatementCallback; bad SQL grammar [select username,authority from authorities where username = ?]; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'username' in 'field list'"
}

Apparently is still using the default JdbcDaoImpl. In fact, when i started debugging, i found that is following these steps:

  1. Authenticates client (OK, since i haven't modified oauth_client_details table)
  2. Authenticates user with my custom userDetailsService(OK, users table modificated but my custom userDetailsService supports changes)
  3. Authenticates user with default userDetailsService(ERROR)

I don't know why this is happening. It sounds like a bug to me. Do you find anything wrong?

jscherman
  • 5,839
  • 14
  • 46
  • 88

3 Answers3

4

You are using auth.jdbcAuthentication().dataSource(this.dataSource).and().userDetailsService(‌​this.userService);// Inject custom and I here it's creating two auth managers - one with default JdbcDaoImpl and dataSource pointing to this.dataSource and another with your custom userService. Try just putting auth.userDetailsService(this.userService) (I hope that userService already have jdbc autowired inside).

The point here is that .and() is used to add different authentication configurations to the authentication manager, not configuring the jdbcAuthentication() one.

dzirtbry
  • 328
  • 2
  • 14
1

In 2.0.7 when you do a POST/GET request on /oauth/token with grant type as password, it will actually except a ClientDetailsUserDetailsService but not UserDetailsService.

I had similar issue and this is how I solved it :

public class AppClientDetailsUserDetailsService extends ClientDetailsUserDetailsService {
    public AppClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
        super(clientDetailsService);
    }
}


public class AppConsumerDetailsService implements ClientDetailsService {

     public ClientDetails loadClientByClientId(String clientId)
            throws OAuth2Exception {
           //some logic
     }
}

<http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="authenticationManager"
          entry-point-ref="entryPoint" xmlns="http://www.springframework.org/schema/security"
            >
        <intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY" />
        <anonymous enabled="false" />
        <http-basic entry-point-ref="entryPoint" />
        <custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER" />

</http>

<bean id="clientCredentialsTokenEndpointFilter" class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
      <property name="authenticationManager" ref="authenticationManager" />
    </bean>

authenticationManager is the bean for AppClientDetailsUserDetailsService whose constructor argument is AppConsumerDetailsService.

Karthik
  • 4,950
  • 6
  • 35
  • 65
  • Thanks for your response. I thought that when i did oauth/token request with grant_type password then the authenticationManager the authenticationManager was authenticating the client ( with the ClientDetailsService) in first place and then the user (with the UserDetailsService) . Appart of that, i thought that clientDetailsService just use oauth_ tables (not users, authorities and groups tables), that's why assumed that this was a problem with the UserDetailsService . Is this all wrong? – jscherman Jul 28 '15 at 17:48
  • 1
    First part is correct, It authenticates client and then user. But I did not the second part of your question. By the way do you have any implementation for the `ClientDetailService`? But you have to mention mention `clientCredentialsTokenEndpointFilter` in order to make it work. – Karthik Jul 28 '15 at 17:51
  • no, i don't. i think i should, what do you tnik?. but i can't think of the way to do the client login. i mean, i thought that it was just going to oauth_client_details, i don't understand why should it goes to users table.. – jscherman Jul 28 '15 at 17:55
  • @jscherman can you add the request that you are making to `/oauth/token` in the question? I will try to explain based on that. – Karthik Jul 28 '15 at 18:04
  • so you have `client id` and `client secret` in the call, there should a mechanism to verify whether this is a valid client or not, for that you need a `ClientDetailsUserDetailsService`. – Karthik Jul 28 '15 at 18:16
  • Yes, i understand that. what i really don't is why i cannot use default clientDetailsUserDetailsService, since i didn't modificate oauth_ tables, i think that i don't get how that client authentication works, since i have the thought that it was being done by checking the oauth_client_details (and other oauth_ tables) – jscherman Jul 28 '15 at 18:19
  • Its failing on JdbcDaoImpl line 165 : dbAuthsSet.addAll(loadUserAuthorities(user.getUsername())); which is logical because i changed the table. but this is while authenticating the user, so i don't understand why the clientDetailsUserDetailsService problem you mentioned... – jscherman Jul 28 '15 at 18:38
  • I've just editted explainign better what is happening – jscherman Aug 05 '15 at 19:23
1

The problem is that you are using default JdbcDaoImpl. The query which is causing the problem is

public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
        "select username,authority " +
        "from authorities " +
        "where username = ?";

This is used as a default query to load all authorities for the user by userName inside JdbcDaoImpl. So if you still want to use default JdbcDaoImpl - you can set the custom query, which will do the job with your tables, as the parameter of it.

I'm not sure what is the schema of you users table, but something like this should be similar(if you are configuring JdbcDaoImpl bean programmatically):

String query = "select username,authority " +
        "from authorities join users on users.id = authorities.user_id " +
        "where users.username = ?";
jdbcDaoImpl.setAuthoritiesByUsernameQuery(query);

Or if you create JdbcDaoImpl from XML:

<bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
    <property name="authoritiesByUsernameQuery"
              value="select username,authority from authorities
                        join users on users.id = authorities.user_id
                        where users.username = ?"/>
</bean>

There might be some other default queries that you would like to change to fit your schema, take a look at them inside JdbcDaoImpl.

You might also consider a possibility of writing your own implementation of UserDetailsService if it will start getting too far from default JdbcDaoImpl one.

dzirtbry
  • 328
  • 2
  • 14
  • Actually, that is exactly what i've done. I did my own implementation and injected it, the problem is that is still using defaults one besides my custom userDetailsService. I tried overriding query too, as you say, and happens the same: It's still using the original, so it fails. Thanks anyways – jscherman Aug 06 '15 at 03:01
  • No problem! i appreciate your response either way – jscherman Aug 06 '15 at 03:19
  • 1
    I have another guess: you are using `auth.jdbcAuthentication().dataSource(this.dataSource).and().userDetailsService(this.userService);// Inject custom` and I think here it's creating two auth managers - one with default `JdbcDaoImpl` and dataSource pointing to `this.dataSource` and another with your custom `userService`. Try just putting `auth.userDetailsService(this.userService)` (I hope that `userService` already have jdbc autowired inside). – dzirtbry Aug 06 '15 at 03:40
  • That is a good guess. I will try it tomorrow. But if I take off that them I should inject a client service as well as defaulttokenservices too, right? – jscherman Aug 06 '15 at 04:02
  • it worked! you are a genius! leave your answer so i can give you the bounty points, if you want – jscherman Aug 06 '15 at 15:20