azeckoski azeckoski - 20 days ago 5
Java Question

Configuring 0-legged OAuth 1.0 in Spring Boot

I want to setup a spring boot application with 0-legged (so no request or access tokens) OAuth 1.0. I have been digging around for awhile trying to find an example and I am mostly stuck on how to configure things using the new style (without xml).

For now I just want to get a simple use case working where only 1 path (/oauth) is OAuth protected (everything else is just wide open) and it uses a custom ConsumerDetailsService (see below for the simple version of that code).

Here is my WebSecurityConfigurerAdapter (SecurityConfiguration.java next to my Application.java, which I think is the right way to configure this kind of thing in a spring boot application). I am pretty sure I am missing the provider configuration (as referred to in: http://projects.spring.io/spring-security-oauth/docs/oauth1.html) but my trial-and-error is not yielding results.

@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 0-Legged OAuth on the /oauth and /lti paths only
http.requestMatchers().antMatchers("/oauth"); // .and().... what?
// ??? something must be missing here - provider?
}

}


I also have this in my maven pom.xml:

<!-- security and oauth -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>


My Custom ConsumerDetailsService

@Component
public class LTIConsumerDetailsService implements ConsumerDetailsService {

@Override
public ConsumerDetails loadConsumerByConsumerKey(String consumerKey) throws OAuthException {
BaseConsumerDetails cd;
// TODO really lookup the key and related consumer details, for sample here we just hardcoded
if ("key".equals(consumerKey)) {
cd = new BaseConsumerDetails();
cd.setConsumerKey(consumerKey);
cd.setSignatureSecret(new SharedConsumerSecretImpl("secret"));
cd.setConsumerName("Sample consumerName");
cd.setRequiredToObtainAuthenticatedToken(false); // no token required (0-legged)
cd.setResourceDescription("Sample consumer details - AZ");
cd.setResourceName("Sample resourceName");
} else {
throw new OAuthException("For this example, key must be 'key'");
}
return cd;
}

}


Any suggestions on how to get this working or pointers to spring boot OAuth 1.0 code would be greatly appreciated. Please note that I already tried looking at the separate spring boot security and OAuth guides and wasn't able to merge them successfully.

Answer

Here is how I got 0-legged OAuth 1.0 working in spring-boot 1.1.4 via Java Config.

NOTE: In my case I only wanted OAuth to protect a single path (/oauth/**) so if you want it protecting everything then you may be able to simplify some parts of this. You can see my complete code here: https://github.com/azeckoski/lti_starter

Once you have the minimal parts shown below you should be able to run your spring-boot app and fire an OAuth 1.0 compatible request at /oauth with ConsumerKey:key and Secret:secret and successfully load the path.

Application.java

Important notes: (1) Do not just declare the ZeroLeggedOAuthProviderProcessingFilter as a Bean, if you do that it will end up affecting all paths (it will get picked up by spring automatically) (2) NoAuthConfigurationAdapter has to be there if you want to access the security data outside the protected path (in this case /oauth)

@ComponentScan
@Configuration
@EnableAutoConfiguration
@EnableWebMvcSecurity // enable spring security and web mvc hooks
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class Application extends WebMvcConfigurerAdapter {
    final static Logger log = LoggerFactory.getLogger(Application.class);

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

    // Spring Security

    @Autowired
    @Order(Ordered.HIGHEST_PRECEDENCE + 10)
    @SuppressWarnings("SpringJavaAutowiringInspection")
    public void configureSimpleAuthUsers(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("admin").roles("ADMIN", "USER")
                .and().withUser("user").password("user").roles("USER");
    }

    @Configuration
    @Order(1) // HIGHEST
    public static class OAuthSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
        private ZeroLeggedOAuthProviderProcessingFilter zeroLeggedOAuthProviderProcessingFilter;
        @Autowired
        OAuthConsumerDetailsService oauthConsumerDetailsService;
        @Autowired
        OAuthAuthenticationHandler oauthAuthenticationHandler;
        @Autowired
        OAuthProcessingFilterEntryPoint oauthProcessingFilterEntryPoint;
        @Autowired
        OAuthProviderTokenServices oauthProviderTokenServices;
        @PostConstruct
        public void init() {
            zeroLeggedOAuthProviderProcessingFilter = new ZeroLeggedOAuthProviderProcessingFilter(oauthConsumerDetailsService, new InMemoryNonceServices(), oauthProcessingFilterEntryPoint, oauthAuthenticationHandler, oauthProviderTokenServices);
        }
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/oauth/**")
                    .addFilterBefore(zeroLeggedOAuthProviderProcessingFilter, UsernamePasswordAuthenticationFilter.class)
                    .authorizeRequests().anyRequest().hasRole("OAUTH");
        }
    }

    @Order(45) // LOW
    @Configuration
    public static class BasicAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/basic/**").authorizeRequests().anyRequest().authenticated()
                    .and().httpBasic();
        }
    }

    @Order(67) // LOWEST
    @Configuration
    public static class NoAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/**").authorizeRequests().anyRequest().permitAll();
        }
    }

    // OAuth beans

    public static class OAuthProcessingFilterEntryPointImpl extends OAuthProcessingFilterEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            log.info("OAuth FILTER Failure (commence), req=" + request + ", ex=" + authException);
            // Called when there is an OAuth Auth failure, authException may be InsufficientAuthenticationException
            super.commence(request, response, authException);
        }
    }

    @Bean(name = "oauthAuthenticationEntryPoint")
    public OAuthProcessingFilterEntryPoint oauthAuthenticationEntryPoint() {
        return new OAuthProcessingFilterEntryPointImpl();
    }

    @Bean(name = "oauthProviderTokenServices")
    public OAuthProviderTokenServices oauthProviderTokenServices() {
        // NOTE: we don't use the OAuthProviderTokenServices for 0-legged but it cannot be null
        return new InMemoryProviderTokenServices();
    }

    public static class ZeroLeggedOAuthProviderProcessingFilter extends ProtectedResourceProcessingFilter {
        ZeroLeggedOAuthProviderProcessingFilter(OAuthConsumerDetailsService oAuthConsumerDetailsService, OAuthNonceServices oAuthNonceServices, OAuthProcessingFilterEntryPoint oAuthProcessingFilterEntryPoint, OAuthAuthenticationHandler oAuthAuthenticationHandler, OAuthProviderTokenServices oAuthProviderTokenServices) {
            super();
            log.info("CONSTRUCT Zero Legged OAuth provider");
            setAuthenticationEntryPoint(oAuthProcessingFilterEntryPoint);
            setAuthHandler(oAuthAuthenticationHandler);
            setConsumerDetailsService(oAuthConsumerDetailsService);
            setNonceServices(oAuthNonceServices);
            setTokenServices(oAuthProviderTokenServices);
            //setIgnoreMissingCredentials(false); // die if OAuth params are not included
        }
    }
}

OAuthConsumerDetailsService.java

@Component
public class OAuthConsumerDetailsService implements ConsumerDetailsService {
    final static Logger log = LoggerFactory.getLogger(OAuthConsumerDetailsService.class);

    @Override
    public ConsumerDetails loadConsumerByConsumerKey(String consumerKey) throws OAuthException {
        BaseConsumerDetails cd;
        // NOTE: really lookup the key and secret, for the sample here we just hardcoded
        if ("key".equals(consumerKey)) {
            // allow this oauth request
            cd = new BaseConsumerDetails();
            cd.setConsumerKey(consumerKey);
            cd.setSignatureSecret(new SharedConsumerSecretImpl("secret"));
            cd.setConsumerName("Sample");
            cd.setRequiredToObtainAuthenticatedToken(false); // no token required (0-legged)
            cd.getAuthorities().add(new SimpleGrantedAuthority("ROLE_OAUTH")); // add the ROLE_OAUTH (can add others as well)
            log.info("OAuth check SUCCESS, consumer key: " + consumerKey);
        } else {
            // deny - failed to match
            throw new OAuthException("For this example, key must be 'key'");
        }
        return cd;
    }

}

MyOAuthAuthenticationHandler.java

This last part is important to define the actual user (and Principal) based on the data coming in from the OAuth request. This is going to vary depending on how you handle things locally but this is an example of how to do it.

@Component
public class MyOAuthAuthenticationHandler implements OAuthAuthenticationHandler {    
    final static Logger log = LoggerFactory.getLogger(MyOAuthAuthenticationHandler.class);

    static SimpleGrantedAuthority userGA = new SimpleGrantedAuthority("ROLE_USER");
    static SimpleGrantedAuthority adminGA = new SimpleGrantedAuthority("ROLE_ADMIN");

    @Override
    public Authentication createAuthentication(HttpServletRequest request, ConsumerAuthentication authentication, OAuthAccessProviderToken authToken) {
        Collection<GrantedAuthority> authorities = new HashSet<>(authentication.getAuthorities());
        // attempt to create a user Authority
        String username = request.getParameter("username");
        if (StringUtils.isBlank(username)) {
            username = authentication.getName();
        }

        // NOTE: you should replace this block with your real rules for determining OAUTH ADMIN roles
        if (username.equals("admin")) {
            authorities.add(userGA);
            authorities.add(adminGA);
        } else {
            authorities.add(userGA);
        }

        Principal principal = new NamedOAuthPrincipal(username, authorities,
                authentication.getConsumerCredentials().getConsumerKey(),
                authentication.getConsumerCredentials().getSignature(),
                authentication.getConsumerCredentials().getSignatureMethod(),
                authentication.getConsumerCredentials().getSignatureBaseString(),
                authentication.getConsumerCredentials().getToken()
        );
        Authentication auth = new UsernamePasswordAuthenticationToken(principal, null, authorities);
        return auth;
    }

    public static class NamedOAuthPrincipal extends ConsumerCredentials implements Principal {
        public String name;
        public Collection<GrantedAuthority> authorities;

        public NamedOAuthPrincipal(String name, Collection<GrantedAuthority> authorities, String consumerKey, String signature, String signatureMethod, String signatureBaseString, String token) {
            super(consumerKey, signature, signatureMethod, signatureBaseString, token);
            this.name = name;
            this.authorities = authorities;
        }

        @Override
        public String getName() {
            return name;
        }

        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    }
}

OAuthController.java

@Controller
@RequestMapping("/oauth")
public class OAuthController extends BaseController {

    @RequestMapping({"", "/"})
    public String home(HttpServletRequest req, Principal principal, Model model) {
        return "home"; // name of the template
    }
}

pom.xml (maven - only the key parts)

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security and oauth -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth -->
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth</artifactId>
  <version>2.0.2.RELEASE</version>
</dependency>