9

I would have one question regarding the configuration of spring-security-oauth2 2.0.7 please. I am doing the Authentication using LDAP via a GlobalAuthenticationConfigurerAdapter:

@SpringBootApplication
@Controller
@SessionAttributes("authorizationRequest")
public class AuthorizationServer extends WebMvcConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(AuthorizationServer.class, args);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/oauth/confirm_access").setViewName("authorize");
    }

    @Configuration
    public static class JwtConfiguration {

        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            KeyPair keyPair = new KeyStoreKeyFactory(
                    new ClassPathResource("keystore.jks"), "foobar".toCharArray())
                    .getKeyPair("test");
            converter.setKeyPair(keyPair);
            return converter;
        }

        @Bean
        public JwtTokenStore jwtTokenStore(){
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    }


    @Configuration
    @EnableAuthorizationServer
    public static class OAuth2Config extends AuthorizationServerConfigurerAdapter implements EnvironmentAware {

        private static final String ENV_OAUTH = "authentication.oauth.";
        private static final String PROP_CLIENTID = "clientid";
        private static final String PROP_SECRET = "secret";
        private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";

        private RelaxedPropertyResolver propertyResolver;

        @Inject
        private AuthenticationManager authenticationManager;

        @Inject
        private JwtAccessTokenConverter jwtAccessTokenConverter;

        @Inject
        private JwtTokenStore jwtTokenStore;

        @Inject
        private UserDetailsService userDetailsService;

        @Override
        public void setEnvironment(Environment environment) {
            this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
        }

        @Bean
        @Primary
        public DefaultTokenServices tokenServices() {
            DefaultTokenServices tokenServices = new DefaultTokenServices();
            tokenServices.setSupportRefreshToken(true);
            tokenServices.setTokenStore(jwtTokenStore);
            tokenServices.setAuthenticationManager(authenticationManager);
            return tokenServices;
        }


        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager).tokenStore(jwtTokenStore).accessTokenConverter(
                    jwtAccessTokenConverter).userDetailsService(userDetailsService);
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer)
                throws Exception {
            oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
                    "isAuthenticated()");
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient(propertyResolver.getProperty(PROP_CLIENTID))
                    .scopes("read", "write")
                    .authorities(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER)
                    .authorizedGrantTypes("authorization_code", "refresh_token", "password")
                    .secret(propertyResolver.getProperty(PROP_SECRET))
                    .accessTokenValiditySeconds(propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer.class, 1800));
        }
    }

    @Configuration
    @Order(-10)
    protected static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .formLogin().loginPage("/login").permitAll()
                    .and()
                    .requestMatchers().antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access")
                    .and()
                    .authorizeRequests().anyRequest().authenticated();
        }

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

        @Bean
        @Override
        public UserDetailsService userDetailsServiceBean() throws Exception {
            return super.userDetailsServiceBean();
        }
    }

    @Configuration
    protected static class AuthenticationConfiguration extends
            GlobalAuthenticationConfigurerAdapter {

        @Override
        public void init(AuthenticationManagerBuilder auth) throws Exception {
            auth
                    .ldapAuthentication()
                    .userDnPatterns("uid={0},ou=people")
                    .groupSearchBase("ou=groups")
                    .contextSource().ldif("classpath:test-server.ldif");
        }
    }
}

While the refresh token works fine with the release 2.0.6 of spring-security-oauth2, it does not work anymore with the version 2.0.7. As read here, one should set the AuthenticationManager to be used when trying to get a new access token during the refresh.

As far as I understand, this has something to do with the following change of spring-security-oauth2.

I unfortunately did not manage to set it up properly.

org.springframework.security.oauth2.provider.token.DefaultTokenServices#setAuthenticationManager

is called and gets an AuthenticationManager injected. I an not sure I understand how the LdapUserDetailsService is then going to be injected. The only thing I see is that the PreAuthenticatedAuthenticationProvider is going to be called while trying to re-authenticate the user during the token refresh call.

Can anyone advise me on how to do it please?

ps: The exception I am getting is the following:

p.PreAuthenticatedAuthenticationProvider : PreAuthenticated authentication request: org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken@5775: Principal: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d5545: Principal: bob; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
o.s.s.o.provider.endpoint.TokenEndpoint  : Handling error: IllegalStateException, UserDetailsService is required.
Jérémie
  • 557
  • 4
  • 15
  • 1
    As described [here](https://github.com/spring-projects/spring-security-oauth/blob/e303652a911cf0168b5be68383ac120c23ec63b3/docs/oauth2.md), `userDetailsService: if you inject a UserDetailsService or if one is configured globally anyway (e.g. in a GlobalAuthenticationManagerConfigurer) then a refresh token grant will contain a check on the user details, to ensure that the account is still active.` I would have expected the `LdapUserDetailsService` to be automatically used. – Jérémie May 26 '15 at 09:38
  • So would I (and it works for me). Can you post a complete project? – Dave Syer May 28 '15 at 13:13
  • @dave-syer, Thanks a lot for your reply. I isolated the problem in the following test project: https://github.com/jhoelter/zaas/tree/master/authserver there is one tag called **spring-security-oauth2-2.0.6** where the refresh token works fine. When switching to 2.0.7 and setting the above described config it crashes. Many Thanks in advance for you help – Jérémie Jun 02 '15 at 10:04
  • example curl request I use: `curl -u testClient localhost:9999/uaa/oauth/token \ -d grant_type=refresh_token -d client_id=testClient \ -d refresh_token=[jwt-refresh-token]` – Jérémie Jun 02 '15 at 10:04
  • I get an HTTP Status 500, `{"error":"server_error","error_description":"UserDetailsService is required."}` – Jérémie Jun 02 '15 at 10:08
  • user: bob / password: pw – Jérémie Jun 02 '15 at 10:16
  • 1
    OK I see the problem. `LdapAuthenticationProvider` is not backed by a `UserDetailsService` so you don't actually have one anywhere (other than an empty delegate in your filter chain). I'll play with it a bit and see if there is a workaround, or a new feature we can add. – Dave Syer Jun 03 '15 at 10:30
  • 1
    I'm having the same problem, Spring framework is a nightmare! Because I'm using an AuthenticationProvider implementation instead of a UserDetailService, but if I downgrade the library to 2.0.6 like Jereremie says, it's works, but now the login with `grant_type=user_credentials` doesn't work, Spring doesn't map the authorities and I don't know why because the roles are there, it can viewed with any JWT tool when you decode the token. – Mariano Ruiz Jul 29 '16 at 23:06

3 Answers3

5

I had a similar issue when I was implementing a a OAuth2 server with JWT tokens with a custom AuthenticationProvider instead of a UserDetailsService implementation to solve login authentications.

But lately I found that the error Spring raises is correct if you want the refresh_token working correctly. For an AuthenticationProvider implementation is impossible to refresh a token with a refresh_token, because in that kind of implementation you have to resolve if the password is correct, but the refresh token doesn't have that information. However, UserDetailsService is agnostic of the password.

The version 2.0.6 of spring-security-oauth2 works because never checks the user grants, just checks if the refresh token is valid (signed with the private key), but, if the user was deleted from the system after a first login, with a refresh token the deleted user will have infinite time access to your system, that is a big security issue.

Take a look to the issue I reported with this: https://github.com/spring-projects/spring-security-oauth/issues/813

Mariano Ruiz
  • 4,314
  • 2
  • 38
  • 34
4

What you need for the OAuth piece is to create an LdapUserDetailsService with the same query as you authenticator and inject it into the AuthorizationServerEndpointsConfigurer. I don't think there's any support for creating a UserDetailService in @Configuration style (might be worth opening a ticket for that in JIRA), but it looks like you can do it in XML.

Dave Syer
  • 56,583
  • 10
  • 155
  • 143
  • Hi Dave, Thanks a lot for your feedback. I setup a Custom `LdapUserDetailsService` (using an old style XML ApplicationContext). The implementation can be found under this [tag](https://github.com/jhoelter/zaas/releases/tag/custom-LdapUserDetailsService). – Jérémie Jun 11 '15 at 07:38
  • I will see to create a ticket for this. I find it quite heavy to have to create a second `LdapUserDetailsService` even if one was automatically configured while calling `org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder#ldapAuthentication` – Jérémie Jun 11 '15 at 07:42
  • This post helped me a lot. I'm on an older version of Spring Security (1.x) but was having the same problem. I realized we have a @Bean foe UserDetailsService which for everything else seemed to be working. But I needed to add configuration to the Configurer to know about the UserDetailsService. Thanks for this comment, helping years later. – Kevin M May 06 '19 at 17:43
4

As advised by Dave Syer, I created a custom LdapUserDetailsService. The working solution can be found under the following tag.

Application Context

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>
    <context:property-placeholder location="application.yml"/>

    <bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
        <constructor-arg value="${authentication.ldap.url}" />
    </bean>

    <bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
        <constructor-arg index="0" value="${authentication.ldap.userSearchBase}" />
        <constructor-arg index="1" value="uid={0}" />
        <constructor-arg index="2" ref="contextSource"/>
    </bean>

    <bean id="ldapAuthoritiesPopulator" class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
        <constructor-arg index="0" ref="contextSource"/>
        <constructor-arg index="1" value="${authentication.ldap.groupSearchBase}"/>
        <property name="groupSearchFilter" value="${authentication.ldap.groupSearchFilter}"/>
    </bean>

    <bean id="myUserDetailsService"
          class="org.springframework.security.ldap.userdetails.LdapUserDetailsService">
        <constructor-arg index="0" ref="userSearch"/>
        <constructor-arg index="1" ref="ldapAuthoritiesPopulator"/>
    </bean>

</beans>

Properties

authentication:
 ldap:
  url: ldap://127.0.0.1:33389/dc=springframework,dc=org
  userSearchBase:
  userDnPatterns: uid={0},ou=people
  groupSearchBase: ou=groups
  groupSearchFilter: (uniqueMember={0})
Jérémie
  • 557
  • 4
  • 15