1

I'm trying to create an Oauth authentication/authorization server using spring boot and dependencies * spring-security-oauth2-autoconfigure * nimbus-jose-jwt

and I'm following docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2-authorization-server

The issue is that I don't want to specify a UserDetailsService since the information about the user account is in another service that doesn't expose passwords. That service just has an API in which input is user/pass and output is user info (if the user exists/credentials are correct).

So my code/configuration is a little deviated from the documentation.

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

     //injections

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

and

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

   //injections

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

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
        authenticationManagerBuilder.authenticationProvider(travelerAuthenticationProvider); //my custom // authentication provider that calls the other service for checking credentials
    }
}

and

@Component
public class TravelerAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TravelerAuthenticationProvider.class);

    private OrderTravelerProfileClient travelerProfileClient;

    public TravelerAuthenticationProvider(OrderTravelerProfileClient travelerProfileClient) {
        this.travelerProfileClient = travelerProfileClient;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (authentication.getName() == null || (authentication.getCredentials().toString().isEmpty())) {
            return null;
        }
        var username = authentication.getName();
        var password = authentication.getCredentials().toString();
        try {
            travelerProfileClient.authenticate(username, password);
        } catch (Exception e) {
            LOGGER.error("checking traveler {} credentials failed", username, e);
            throw new BadCredentialsException("wrong traveler credentials");
        }
        var authorities = Set.of(new SimpleGrantedAuthority("traveler"));
        var updatedAuthentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
        return updatedAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

Everything related to client_credentials and password flow works but when I try to use refresh_token flow, it complains that UserDetailsService is required. How should I solve the issue without defining a UserDetailsService and just relaying on my custom authentication provider?

UPDATE: apparently refresh_token flow has a recheck for authentication (credentials) which needs another authentication provider for type PreAuthenticatedAuthenticationToken.class.

So I created a new auth provider like this:

@Component
public class TravelerRefreshTokenBasedAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TravelerRefreshTokenBasedAuthenticationProvider.class);


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        var currentAuthentication = (PreAuthenticatedAuthenticationToken) authentication;
            //.....
        return updatedAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(PreAuthenticatedAuthenticationToken.class);
    }
}

and update my security configs to:

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

//injections

    //this bean will be more configured by the method below and it will be used by spring boot
    //for authenticating requests. Its kind of an equivalent to userDetailsService
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
        authenticationManagerBuilder.authenticationProvider(travelerUserPassBasedAuthenticationProvider);
        authenticationManagerBuilder.authenticationProvider(travelerRefreshTokenBasedAuthenticationProvider);
    }
}

the issue is spring doesn't recognize my auth providers in refresh_token flow and tries to use a default one. And the default one is trying to use a UserDetailsService that doesn't exist.

I also feel that I don't need to create another provider and I can reuse the previous one. Because the check for which spring is failing to use my custom provider is a check against user/pass; which I was doing in my previous auth provider.

so all in all, until now, I feel I have to introduce my custom provider to spring differently for refresh_token flow comparing to password flow

Mahdi Amini
  • 402
  • 1
  • 3
  • 17

1 Answers1

1

Your AuthenticationProvider implementation only supports UsernamePasswordAuthenticationToken, which is used for username/password authentication, while the refresh_token flow tries to renew authentication using PreAuthenticatedAuthenticationToken (see DefaultTokenServices.java).

So you need to create another AuthenticationProvider for PreAuthenticatedAuthenticationToken and add it to AuthenticationManagerBuilder.

Update:

I've found that AuthorizationServerEndpointsConfigurer creates a new instance of DefaultTokenServices, if none is assigned, which in turn creates a new instance of PreAuthenticatedAuthenticationProvider and does not use the provided AuthenticationManager. To avoid this, you can create your own instance of DefaultTokenServices and pass it to AuthorizationServerEndpointsConfigurer:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints
            .tokenStore(jwtTokenStore)
            .accessTokenConverter(accessTokenConverter)
            .tokenEnhancer(accessTokenConverter)
            .authenticationManager(authenticationManager)
            .tokenServices(createTokenServices(endpoints, authenticationManager));
}

private DefaultTokenServices createTokenServices(AuthorizationServerEndpointsConfigurer endpoints, AuthenticationManager authenticationManager) {
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    tokenServices.setSupportRefreshToken(true);
    tokenServices.setTokenStore(endpoints.getTokenStore());
    tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
    tokenServices.setAuthenticationManager(authenticationManager);
    return tokenServices;
}
Anar Sultanov
  • 3,016
  • 2
  • 17
  • 27
  • I tried it. no difference, unfortunately. My new AuthPrivoder is not called. @anar-sultanov – Mahdi Amini Nov 19 '19 at 11:01
  • ```@Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) { authenticationManagerBuilder.authenticationProvider(travelerUserPassBasedAuthenticationProvider); authenticationManagerBuilder.authenticationProvider(travelerRefreshTokenBasedAuthenticationProvider); }``` – Mahdi Amini Nov 19 '19 at 11:19
  • I debugged the library side code (starting from the code that you linked in your example). none of my auth providers are being recognized in refresh_token flow although I introduced it to AuthenticationManagerBuilder – Mahdi Amini Nov 19 '19 at 11:20
  • ``` @Component public class TravelerRefreshTokenBasedAuthenticationProvider implements AuthenticationProvider { //....authentication method @Override public boolean supports(Class> authentication) { return authentication.equals(PreAuthenticatedAuthenticationToken.class); } } ``` – Mahdi Amini Nov 19 '19 at 11:24
  • @NeoSmith It would be more convenient if you update your answer or, even better, create a minimal reproducible example and upload it to GitHub. A little later I will try to reproduce the problem and update my answer. – Anar Sultanov Nov 19 '19 at 11:31
  • I will update my question here. There is nothing special in the service other than configurations and auth providers. thanks very much – Mahdi Amini Nov 19 '19 at 11:37
  • thank you man. It looks like the answer to my issue. But as I'm using JWT, I guess I need to somehow write my own token enhancer. Because with default one I get ``` "Cannot convert access token to JSON"``` – Mahdi Amini Nov 19 '19 at 14:10
  • I used my JWTTokenStore instead of endpoints.getTokenStore(). Because using endpoints.getTokenStore() doesn't put JWT configurations into action – Mahdi Amini Nov 19 '19 at 14:12
  • Yes I used JWTTokenConvertor.enhance() and craeted my own token enhancer and then it worked – Mahdi Amini Nov 19 '19 at 14:20
  • 1
    @NeoSmith I guess your `accessTokenConverter` is instance of `JwtAccessTokenConverter` which implements `TokenEnhancer`. So you can pass it to endpoints and it will be picked up. Corrected the answer, could you please accept it if it works for you? – Anar Sultanov Nov 19 '19 at 15:05
  • @NeoSmith can you please upload your solution to GitHub? I am facing similar problem and there is related Open Issue here: https://github.com/spring-projects/spring-security-oauth/issues/1800 – kmandalas Feb 13 '20 at 20:28
  • @kmandalas man dont waste your time with spring-auth-server as it is deprecated. And it won't be continued by spring team any more in any form. I got rid of that code for this reason – Mahdi Amini Feb 16 '20 at 15:45
  • https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update – Mahdi Amini Feb 16 '20 at 16:00
  • @NeoSmith so which approach did you follow? Did you use a 3rd party auth-server? – kmandalas Feb 18 '20 at 10:44
  • I developed very minimalistic jwt issuer/verifier using one of the libraries suggested in jwt.io. it was enough for me then. – Mahdi Amini Feb 19 '20 at 12:55