Skip to content
This repository has been archived by the owner on Apr 5, 2022. It is now read-only.

Documentation or examples for security with proxy #15

Open
ryanjbaxter opened this issue Dec 4, 2014 · 7 comments
Open

Documentation or examples for security with proxy #15

ryanjbaxter opened this issue Dec 4, 2014 · 7 comments
Assignees

Comments

@ryanjbaxter
Copy link
Contributor

See original discussion in #14.

I think if the Zuul proxy somehow knew about the security routes from the resource it is proxying from that would be ideal. That way the configuration stays with the resource and if the resource is accessed without using the proxy the security routes are still in place.

@dsyer
Copy link
Contributor

dsyer commented Dec 4, 2014

I'm not sure it will ever be possible (or advisable) for the proxy to be so tightly coupled with the backends. At present the default is for the proxy to forward the Authentication header from the front end call directly to the backend, and that won't suit all purposes, but it gets you a long way (and won't affect anonymous routes, since they don't, or shouldn't care if they are authenticated or not). If the front end also has @EnableOAuth2Sso (again by default) we switch and add the access token to the backend request, which also seems like a reasonable starting point.

Maybe you could explain your use case and the custom configuration you had to implement in a bit more detail?

@dsyer dsyer added the question label Dec 4, 2014
@dsyer dsyer self-assigned this Dec 4, 2014
@ryanjbaxter
Copy link
Contributor Author

What I am having to do is tell the proxy which routes I want authenticated and which routes I want anonymous and then do the same exact configuration in the resource as well. Code is worth a 1000 words so here it is.

Maybe if we put this configuration in the configuration service and used the service name to configure the routes. Than each service and the proxy can read this configuration from the configuration service and everything is in a central location.

App.java for the proxy

@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableZuulProxy
@EnableEurekaClient
@EnableOAuth2Sso
public class App {

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

  @Component
  public static class LoginConfigurer extends OAuth2SsoConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
      http.requestMatcher(new SecureRequestMatcher()).authorizeRequests().anyRequest().authenticated();
    }
  }

  @Component
  public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      AnonRequestMatcher matcher =new AnonRequestMatcher();
      http.csrf().requireCsrfProtectionMatcher(new SecureRequestMatcher()).and().requestMatcher(matcher).anonymous();
    }

  }
}

class SecureRequestMatcher implements RequestMatcher {
  private Pattern allowedSessionMethods = Pattern.compile("^(POST|PUT|DELETE|HEAD|TRACE|OPTIONS)$");
  private Pattern allowedQuestionMethods = Pattern.compile("^(PUT|DELETE|HEAD|TRACE|OPTIONS)$");
  private Pattern allowedReplyMethods = Pattern.compile("^(POST)$");
  private Pattern allowedSessionApis = Pattern.compile("^(/api/(sessions)(/?[A-za-z0-9]*)*)$");
  private Pattern allowedQuestionApis = Pattern.compile("^(/api/(questions)(/?[A-za-z0-9]*)*)$");
  private Pattern allowedReplyApis = Pattern.compile("^(/api/(reply)(/?[A-za-z0-9]*)*)$");
  @Override
  public boolean matches(HttpServletRequest request) {
    boolean result = "/api/login".equalsIgnoreCase(request.getRequestURI()) ||
            (allowedSessionApis.matcher(request.getRequestURI()).matches() && allowedSessionMethods.matcher(request.getMethod()).matches()) ||
            (allowedQuestionApis.matcher(request.getRequestURI()).matches() && allowedQuestionMethods.matcher(request.getMethod()).matches()) ||
            (allowedReplyApis.matcher(request.getRequestURI()).matches() && allowedReplyMethods.matcher(request.getMethod()).matches());
    return result;
  }

}

class AnonRequestMatcher implements RequestMatcher {
  private Pattern allowedSessionMethods = Pattern.compile("^(GET)$");
  private Pattern allowedQuestionMethods = Pattern.compile("^(POST|GET)$");
  private Pattern allowedSessionApis = Pattern.compile("^(/api/(sessions)(/?[A-za-z0-9]*)*)$");
  private Pattern allowedQuestionApis = Pattern.compile("^(/api/(questions)(/?[A-za-z0-9]*)*)$");
  @Override
  public boolean matches(HttpServletRequest request) {
    boolean result = (allowedSessionApis.matcher(request.getRequestURI()).matches() && allowedSessionMethods.matcher(request.getMethod()).matches()) ||
            (allowedQuestionApis.matcher(request.getRequestURI()).matches() && allowedQuestionMethods.matcher(request.getMethod()).matches()) ||
            "/csrf".equalsIgnoreCase(request.getRequestURI()) && request.getMethod().equals("GET") ||
            "/logout".equalsIgnoreCase(request.getRequestURI());
    return result;
  }
}

App.java from the Sessions resource

@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableEurekaClient
public class App {
  public static void main(String[] args) throws Exception {
      SpringApplication.run(App.class, args);
  }

  @Bean
  public CouchDbConnector couchDbConnector(CouchDbInstance couchDbInstance) {
    CouchDbConnector connector = new StdCouchDbConnector("sessions", couchDbInstance);
    connector.createDatabaseIfNotExists();
    return connector;
  }

  @Component
  public static class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
      http.requestMatcher(new SecureRequestMatcher()).authorizeRequests().anyRequest().authenticated();
    }
  }

  @Component
  public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      AnonRequestMatcher matcher =new AnonRequestMatcher();
      http.csrf().requireCsrfProtectionMatcher(new SecureRequestMatcher()).and().requestMatcher(matcher).anonymous();
    }

  }

}

class SecureRequestMatcher implements RequestMatcher {
  private Pattern allowedSessionMethods = Pattern.compile("^(POST|PUT|DELETE|HEAD|TRACE|OPTIONS)$");
  private Pattern allowedSessionApis = Pattern.compile("^(/(sessions)(/?[A-za-z0-9]*)*)$");
  @Override
  public boolean matches(HttpServletRequest request) {
    boolean result =
            allowedSessionApis.matcher(request.getRequestURI()).matches() && allowedSessionMethods.matcher(request.getMethod()).matches();
    return result;
  }
}

class AnonRequestMatcher implements RequestMatcher {
  private Pattern allowedSessionMethods = Pattern.compile("^(GET)$");
  private Pattern allowedSessionApis = Pattern.compile("^(/(sessions)(/?[A-za-z0-9]*)*)$");
  @Override
  public boolean matches(HttpServletRequest request) {
    boolean result = (allowedSessionApis.matcher(request.getRequestURI()).matches() && allowedSessionMethods.matcher(request.getMethod()).matches());
    return result;
  }
}

App.java from the Questions resource

@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableEurekaClient
public class App {
  public static void main(String[] args) throws Exception {
    SpringApplication.run(App.class, args);
  }

  @Bean
  public CouchDbConnector couchDbConnector(CouchDbInstance couchDbInstance) {
    CouchDbConnector connector = new StdCouchDbConnector("questions", couchDbInstance);
    connector.createDatabaseIfNotExists();
    return connector;
  }

  @Component
  public static class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
      http.requestMatcher(new SecureRequestMatcher()).authorizeRequests().anyRequest().authenticated();
    }
  }

  @Component
  public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      AnonRequestMatcher matcher =new AnonRequestMatcher();
      http.csrf().requireCsrfProtectionMatcher(new SecureRequestMatcher()).and().requestMatcher(matcher).anonymous();
    }

  }
}

class SecureRequestMatcher implements RequestMatcher {
  private Pattern allowedQuestionMethods = Pattern.compile("^(PUT|DELETE|HEAD|TRACE|OPTIONS)$");
  private Pattern allowedQuestionApis = Pattern.compile("^(/(questions)(/?[A-za-z0-9]*)*)$");
  @Override
  public boolean matches(HttpServletRequest request) {
    boolean result = (allowedQuestionApis.matcher(request.getRequestURI()).matches() && allowedQuestionMethods.matcher(request.getMethod()).matches());
    return result;
  }

}

class AnonRequestMatcher implements RequestMatcher {
  private Pattern allowedQuestionMethods = Pattern.compile("^(POST|GET)$");
  private Pattern allowedQuestionApis = Pattern.compile("^(/(questions)(/?[A-za-z0-9]*)*)$");
  @Override
  public boolean matches(HttpServletRequest request) {
    boolean result = (allowedQuestionApis.matcher(request.getRequestURI()).matches() && allowedQuestionMethods.matcher(request.getMethod()).matches());
    return result;
  }
}

@dsyer
Copy link
Contributor

dsyer commented Dec 4, 2014

Can't you just make your whole "/api/**" on the proxy a passthru (ignore security completely or make it anonymous)? Why does the proxy have to protect routes that are already protected by the backend?

@ryanjbaxter
Copy link
Contributor Author

Its late and it is a long day but I can't think of a good reason why right now. Not sure why I didn't think about that, kind of embarrassing. Maybe because I started with securing things at the proxy and then realized that the resource needed to be secured as well, and never went back and thought about this.

I will take a look tomorrow, but I think this simplifies things.

@ryanjbaxter
Copy link
Contributor Author

@dsyer I made it a passthrough as you suggested (added /api/** to security.ignored) but it appears that when doing that it looks like the proxy is not setting the authenticated header when proxying the request to the resource server is rejecting it. I think that makes sense though since we told the proxy to ignore security on those requests.

@dsyer
Copy link
Contributor

dsyer commented Dec 5, 2014

So don't ignore it if you want an authentication header. It should be 'permitAll()' I guess?

@ryanjbaxter
Copy link
Contributor Author

So I was certainly able to simplify the configuration on the proxy end

import javax.servlet.http.HttpServletRequest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.security.oauth2.sso.EnableOAuth2Sso;
import org.springframework.cloud.security.oauth2.sso.OAuth2SsoConfigurerAdapter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;

@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableZuulProxy
@EnableEurekaClient
@EnableOAuth2Sso
public class App {

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

  @Component
  public static class LoginConfigurer extends OAuth2SsoConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
      http.requestMatcher(new RequestMatcher() {

        @Override
        public boolean matches(HttpServletRequest request) {
          boolean result = request.getRequestURI().startsWith("/api") || request.getRequestURI().startsWith("/questions") || "/login".equalsIgnoreCase(request.getRequestURI()) ||
                  "/csrf".equalsIgnoreCase(request.getRequestURI()) && request.getMethod().equals("GET") ||
                  "/logout".equalsIgnoreCase(request.getRequestURI())  || request.getRequestURI().startsWith("/trace");
          return result;
        }

      }).authorizeRequests().anyRequest().permitAll().and().csrf().requireCsrfProtectionMatcher(new RequestMatcher() {

        @Override
        public boolean matches(HttpServletRequest request) {
          //While this doesn't require the proxy endpoints to have a CSRF token on them it does allow our CSRF endpoint
          //to return one back to the client.  If we were to disable csrf entirely at the proxy it would not let the CSRF
          //endpoint return a token back to the client.  The proxy will forward the token to the endpoints it is proxying to
          //and those endpoints have the necessary CSRF configuration.
          return false;
        }

      });
    }
  }
}

I think a complete example that demonstrates how everything should be configured properly with authentication done at the proxy and auth tokens verified at the resource server would be great. I am still not sure if I could simplify the configuration in the resource apps, I tried a few things to eliminate the WebSecurityConfigurerAdapter like I did in the proxy but I couldn't get it to work.

@dsyer dsyer changed the title Simplify Security Routes Documentation or examples for security with proxy Dec 11, 2014
dsyer pushed a commit that referenced this issue Mar 24, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

No branches or pull requests

2 participants