3

I'm trying to get OAuth 1 (3 legged) on a simple Spring Boot + Spring OAuth app, only as a consumer.

I've been trying to port the tonr sample on the spring-security-oauth repository (https://github.com/spring-projects/spring-security-oauth) to use Java config instead of XML.

However, I'm getting:

java.lang.NullPointerException: null
at org.springframework.security.oauth.consumer.filter.OAuthConsumerProcessingFilter.doFilter(OAuthConsumerProcessingFilter.java:87)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:102)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration$MetricsFilter.doFilterInternal(MetricFilterAutoConfiguration.java:90)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:516)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1086)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:659)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:223)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1558)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1515)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)

...probably because the OAuthConsumerContextFilter is not being setup properly.

I tried configuring the <oauth:consumer> part as follows:

@Bean
public OAuthConsumerProcessingFilter oAuthConsumerProcessingFilter()
{
    OAuthConsumerProcessingFilter result = new OAuthConsumerProcessingFilter();
    result.setProtectedResourceDetailsService(protectedResourceDetailsService());

    final LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> map = new LinkedHashMap<>();
    map.put(new RegexRequestMatcher("/sparklr/*", null), Collections.singletonList(ConsumerSecurityConfig.PERMIT_ALL_ATTRIBUTE));
    result.setObjectDefinitionSource(new DefaultFilterInvocationSecurityMetadataSource(map));
    return result;
}

@Bean
public ProtectedResourceDetailsService protectedResourceDetailsService()
{
    return (String id) -> {
        switch (id) {
            case "sparklrPhotos":
                sparklrProtectedResourceDetails();
                break;
        }
        throw new RuntimeException("Error");
    };
}

@Bean
public OAuthConsumerContextFilter oAuthConsumerContextFilter() {
    final CoreOAuthConsumerSupport consumerSupport = new CoreOAuthConsumerSupport();
    consumerSupport.setProtectedResourceDetailsService(protectedResourceDetailsService());

    final OAuthConsumerContextFilter filter = new OAuthConsumerContextFilter();
    filter.setConsumerSupport(consumerSupport);
    return filter;
}

...but obviously something is missing. I even removed the switch and returned the same protected resource details all the time, but that doesn't change the fact that I don't have a context.

What should I do to make this work? Let me know if I need to show any other part of my code.

UPDATE: I've added the Consumer Context filter, but I think it's not being applied, as I get the same error

Edu Garcia
  • 453
  • 7
  • 21
  • Where is your `OAuthConsumerContextFilter`? I have never seen this done with JavaConfig so you are swimming against the stream, but the basic consumer use case is simple enough I guess it ought to work out. – Dave Syer Mar 12 '15 at 11:25
  • There was none on the XML config, so initially I added none, but even when I added one, it wasn't being called. I'll update my question with it – Edu Garcia Mar 12 '15 at 19:41
  • Can you post code where you add OAuthConsumerContextFilter to filters? – Ostap Maliuvanchuk Mar 13 '15 at 14:10
  • I don't explicitly add the filter anywhere, I was expecting Spring to add it somehow, like the `OAuthConsumerProcessingFilter` is being picked up. Also, the XML configuration doesn't specify it so I'd like to know why I have to in the Java one, if that's the problem... – Edu Garcia Mar 13 '15 at 19:30
  • If you look at OAuth2 with Java Config, they add filters explicitly – Ostap Maliuvanchuk Mar 14 '15 at 08:57
  • You still haven't shown how you add the context filter. – Dave Syer Mar 14 '15 at 22:14
  • @DaveSyer and user979349: then that's the part I must be missing and the solution to my problem :). How do I do that? When I use the XML configuration, I see a default chain created with 6 or 7 different filters, but obviously with my Java config I don't see anything. – Edu Garcia Mar 15 '15 at 01:40
  • If it's a Spring Boot app you can add a filter as a `@Bean` definition. I don't think this one needs to be in the Spring Security chain. – Dave Syer Mar 15 '15 at 07:05
  • @DaveSyer It is a Spring Boot app, and the filter is already added as bean, as you can see in my example code, unless you refer to something else – Edu Garcia Mar 15 '15 at 18:38
  • Sorry I missed that. The two filters you have need to be applied in order, so just adding them as beans won't help, unless you also register them with the servlet container. Look at the Spring Boot docs for `ServletRegistrationBean`. – Dave Syer Mar 16 '15 at 07:36

2 Answers2

3

I found that several parts of the code in this question and other answers were incomplete and did not work for various reasons when taken as a whole. Here's the complete solution I found to get Spring Security OAuth 1 working with Spring Boot using Java config.

You need a configuration class like so:

@Configuration
@EnableWebSecurity
public class OAuthConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(
            this.oauthConsumerContextFilter(),
            SwitchUserFilter.class
        );
        http.addFilterAfter(
            this.oauthConsumerProcessingFilter(),
            OAuthConsumerContextFilter.class
        );
    }

    // IMPORTANT: this must not be a Bean
    OAuthConsumerContextFilter oauthConsumerContextFilter() {
        OAuthConsumerContextFilter filter = new OAuthConsumerContextFilter();
        filter.setConsumerSupport(this.consumerSupport());
        return filter;
    }

    // IMPORTANT: this must not be a Bean
    OAuthConsumerProcessingFilter oauthConsumerProcessingFilter() {
        OAuthConsumerProcessingFilter filter = new OAuthConsumerProcessingFilter();
        filter.setProtectedResourceDetailsService(this.prds());

        LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> map =
            new LinkedHashMap<>();

        // one entry per oauth:url element in xml
        map.put(
            // 1st arg is equivalent of url:pattern in xml
            // 2nd arg is equivalent of url:httpMethod in xml
            new AntPathRequestMatcher("/oauth/**", null),
            // arg is equivalent of url:resources in xml
            // IMPORTANT: this must match the ids in prds() and prd() below
            Collections.singletonList(new SecurityConfig("myResource"))
        );

        filter.setObjectDefinitionSource(
            new DefaultFilterInvocationSecurityMetadataSource(map)
        );

        return filter;
    }

    @Bean // optional, I re-use it elsewhere, hence the Bean
    OAuthConsumerSupport consumerSupport() {
        CoreOAuthConsumerSupport consumerSupport = new CoreOAuthConsumerSupport();
        consumerSupport.setProtectedResourceDetailsService(prds());
        return consumerSupport;
    }

    @Bean // optional, I re-use it elsewhere, hence the Bean
    ProtectedResourceDetailsService prds() {
        return (String id) -> {
            switch (id) {
            // this must match the id in prd() below
            case "myResource":
                return prd();
            }
            throw new RuntimeException("Invalid id: " + id);
        };
    }

    ProtectedResourceDetails prd() {
        BaseProtectedResourceDetails details = new BaseProtectedResourceDetails();

        // this must be present and match the id in prds() and prd() above
        details.setId("myResource");

        details.setConsumerKey("OAuth_Key");
        details.setSharedSecret("OAuth_Secret");

        details.setRequestTokenURL("...");
        details.setUserAuthorizationURL("...");
        details.setAccessTokenURL("...");

        // any other service-specific settings

        return details;
    }
}

Some key points to understand so you can avoid the problems I faced:

First, the original question configures the Processing filter with ConsumerSecurityConfig.PERMIT_ALL_ATTRIBUTE but this doesn't work. The value in the map has to be a SecurityConfig containing the id of the resource which is the OAuth owner for those paths.

This id must also match the ids in both the ProtectedResourceDetailsService and the ProtectedResourceDetails, and both ids must be present. Even though they are somewhat redundant, leaving out the id in ProtectedResourceDetails breaks the setup.

Second, the original question configures both filters as Beans. However, Spring Boot automatically registers any Beans which implement Filter in the main application filter chain which in this case will cause them to run twice and fail the OAuth access token process every time since it looks like a replay attack. More details in this question: Oauth 1.0a consumer code equesting an access token twice

There are two ways to fix this. I used the shorter one above (just don't make them Beans) but you can also create a FilterRegistrationBean to disable the auto-registration behavior.

With those corrections in place, the code above is tested and confirmed to support a working OAuth 1 negotiation with pure Java config in a Spring Boot container.

References:

The Spring OAuth 1 doc is good for understanding the classes involved and what they're for: https://projects.spring.io/spring-security-oauth/docs/oauth1.html

The Bean Definition Parser is the canonical implementation which converts XML to Java, so see this for any edge cases I didn't mention: https://github.com/codehaus/spring-security-oauth/blob/master/spring-security-oauth/src/main/java/org/springframework/security/oauth/config/OAuthConsumerBeanDefinitionParser.java

enroth
  • 101
  • 7
2

In order to use Spring Security with Java Config you have to have SecurityConfig file with something like this inside (taken from http://projects.spring.io/spring-security-oauth/docs/oauth2.html)

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests().antMatchers("/login").permitAll().and()
    // default protection for all resources (including /oauth/authorize)
        .authorizeRequests()
            .anyRequest().hasRole("USER")
    // ... more configuration, e.g. for form login
}

That's also a place where you can add your filters in specific order using http.addFilterAfter(oAuthConsumerContextFilter(), AnonymousAuthenticationFilter.class);

The problem with your code is that your filter is being executed before Authetication created.

So I guess both of yout filters should be at least after AnonymousAuthenticationFilter.class

You can find list of filters here : http://docs.spring.io/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#filter-stack

This worked for me :

http
.addFilterAfter(oAuthConsumerContextFilter(), SwitchUserFilter.class)
.addFilterAfter(oAuthConsumerProcessingFilter(), OAuthConsumerContextFilter.class)
Ostap Maliuvanchuk
  • 1,125
  • 2
  • 12
  • 32
  • That was it, thanks a lot! I saw that `addFilterAfter` and `addFilterBefore` but didn't think I had to manually add everything, seeing how the XML configuration doesn't even define the context filter. Anyway, thanks again! – Edu Garcia Mar 17 '15 at 21:55