Simeon Leyzerzon
Simeon Leyzerzon

Reputation: 19074

Endpoint protection configuration via @EnableGlobalMethodSecurity in Spring Boot

While trying to recreate a sample from a book demonstrating how to restrict access to an endpoint, I'm getting an unexpected behavior - an admin with a permitting role is disallowed access:

    $ curl -X POST \
>   'http://localhost:9090/oauth/token?grant_type=password&username=admin&password=password2' \
>   -H 'authorization: Basic d2ViYXBwOndlYnNlY3JldA==' \
>   -H 'cache-control: no-cache' \
>   -d '"category":"test","document":"this is a test document"'
    {"access_token":"6d149c21-6a48-41e8-885d-d6da70648b49","token_type":"bearer","expires_in":42860,"scope":"read,write,trust"}

$ curl -X GET \
>   'http://localhost:9090/resource?access_token=6d149c21-6a48-41e8-885d-d6da70648b49' \
>   -H 'cache-control: no-cache' \

{"timestamp":1508464945487,"status":403,"error":"Forbidden","exception":"org.springframework.security.access.AccessDeniedException","message":"Access Denied","path":"/resource"} 

Here are the relevant classes:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user1").password("password1").roles("USER")
        .and().withUser("admin").password("password2").roles("ADMIN");
    }

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

}

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authManager;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authManager);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("webapp").secret("websecret").authorizedGrantTypes("password")
                .scopes("read,write,trust");
    }



}

@SpringBootApplication
@EnableAuthorizationServer
@EnableResourceServer
@RestController
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class OauthServerApplication {


    @RequestMapping("/resource")
    @PreAuthorize("hasRole('ADMIN')")
    public String resourceEndpoint() {
        return "This resource is protected by the resource server.";
    }

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

What am I missing here?

The goal is to have only adminuser to be able to access the resource endpoint, while the regular user user1is to be denied access.

N.B.: the below stanza is actually something I added:

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

as its absence produces the following during resource acquisition attempt (2nd request):

<html>
    <head>
        <title>Login Page</title>
    </head>
    <body onload='document.f.username.focus();'>
        <h3>Login with Username and Password</h3>
        <form name='f' action='/login' method='POST'>
            <table>
                <tr>
                    <td>User:</td>
                    <td>
                        <input type='text' name='username' value=''>
                    </td>
                </tr>
                <tr>
                    <td>Password:</td>
                    <td>
                        <input type='password' name='password'/>
                    </td>
                </tr>
                <tr>
                    <td colspan='2'>
                        <input name="submit" type="submit" value="Login"/>
                    </td>
                </tr>
                <input name="_csrf" type="hidden" value="1cbdad0b-181e-496c-aed0-eb633b29eab7" />
            </table>
        </form>
    </body>
</html>

After enabling web security log, as some of the commentators suggested, like so:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

.....

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.debug(true);
    } 
...}

here's the partial resulting server log from the 2 requests:

2017-10-20 07:17:27.836  WARN 12728 --- [           main] o.s.s.c.a.web.builders.WebSecurity       : 

********************************************************************
**********        Security debugging is enabled.       *************
**********    This may include sensitive information.  *************
**********      Do not use in a production system!     *************
********************************************************************


2017-10-20 07:17:29.116  INFO 12728 --- [           main] o.s.cloud.commons.util.InetUtils         : Cannot determine local hostname
2017-10-20 07:17:29.180  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-10-20 07:17:29.186  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'configurationPropertiesRebinder' has been autodetected for JMX exposure
2017-10-20 07:17:29.187  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'refreshEndpoint' has been autodetected for JMX exposure
2017-10-20 07:17:29.187  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'restartEndpoint' has been autodetected for JMX exposure
2017-10-20 07:17:29.187  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'environmentManager' has been autodetected for JMX exposure
2017-10-20 07:17:29.188  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'refreshScope' has been autodetected for JMX exposure
2017-10-20 07:17:29.189  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'environmentManager': registering with JMX server as MBean [org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager]
2017-10-20 07:17:29.197  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'restartEndpoint': registering with JMX server as MBean [org.springframework.cloud.context.restart:name=restartEndpoint,type=RestartEndpoint]
2017-10-20 07:17:29.202  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'refreshScope': registering with JMX server as MBean [org.springframework.cloud.context.scope.refresh:name=refreshScope,type=RefreshScope]
2017-10-20 07:17:29.207  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'configurationPropertiesRebinder': registering with JMX server as MBean [org.springframework.cloud.context.properties:name=configurationPropertiesRebinder,context=6f7923a5,type=ConfigurationPropertiesRebinder]
2017-10-20 07:17:29.211  INFO 12728 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'refreshEndpoint': registering with JMX server as MBean [org.springframework.cloud.endpoint:name=refreshEndpoint,type=RefreshEndpoint]
2017-10-20 07:17:29.338  INFO 12728 --- [           main] o.s.c.support.DefaultLifecycleProcessor  : Starting beans in phase 0
2017-10-20 07:17:29.402  INFO 12728 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 9090 (http)
2017-10-20 07:17:29.406  INFO 12728 --- [           main] c.e.spring.cloud.OauthServerApplication  : Started OauthServerApplication in 6.879 seconds (JVM running for 7.264)
2017-10-20 07:18:20.584  INFO 12728 --- [nio-9090-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-10-20 07:18:20.584  INFO 12728 --- [nio-9090-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2017-10-20 07:18:20.600  INFO 12728 --- [nio-9090-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 16 ms
2017-10-20 07:18:20.606  INFO 12728 --- [nio-9090-exec-1] Spring Security Debugger                 : 

************************************************************

Request received for POST '/oauth/token?grant_type=password&username=user1&password=password1':

org.apache.catalina.connector.RequestFacade@5ace935e

servletPath:/oauth/token
pathInfo:null
headers: 
host: localhost:9090
connection: keep-alive
content-length: 54
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
cache-control: no-cache
origin: chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop
content-type: text/plain;charset=UTF-8
authorization: Basic d2ViYXBwOndlYnNlY3JldA==
postman-token: cd55952b-6c6f-6101-8f24-3942dee9b06a
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=F4526A8B6FD15FD35D3D84D25E2C3898


Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  LogoutFilter
  BasicAuthenticationFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]


************************************************************


2017-10-20 07:20:18.604  INFO 12728 --- [nio-9090-exec-4] Spring Security Debugger                 : 

************************************************************

Request received for GET '/resource?access_token=5466dfff-f088-4097-8db9-4ed07f0b80a0':

org.apache.catalina.connector.RequestFacade@5ace935e

servletPath:/resource
pathInfo:null
headers: 
host: localhost:9090
connection: keep-alive
cache-control: no-cache
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
postman-token: 394b6610-1d18-eec7-338b-ab1de65cfeeb
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=F4526A8B6FD15FD35D3D84D25E2C3898


Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CsrfFilter
  LogoutFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]


************************************************************


2017-10-20 07:20:18.640  INFO 12728 --- [nio-9090-exec-4] Spring Security Debugger                 : 

************************************************************

New HTTP session created: 1D899B6306F970E097CE746030A28E4A

Call stack: 

    at org.springframework.security.web.debug.Logger.info(Logger.java:44)
    at org.springframework.security.web.debug.DebugRequestWrapper.getSession(DebugFilter.java:166)
    at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:240)
    at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:240)
    at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:240)
    at org.springframework.security.web.savedrequest.HttpSessionRequestCache.saveRequest(HttpSessionRequestCache.java:59)
    at org.springframework.security.web.access.ExceptionTranslationFilter.sendStartAuthentication(ExceptionTranslationFilter.java:201)
    at org.springframework.security.web.access.ExceptionTranslationFilter.handleSpringSecurityException(ExceptionTranslationFilter.java:177)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:133)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:170)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
    at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:90)
    at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:77)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:108)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:106)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1457)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)


************************************************************

When I comment out the @PreAuthorize("hasRole('ADMIN')") annotation on top of the resource endpoint, the resource request correctly displays "This resource is protected by the resource server." Also a breakpoint placed in there gets triggered. Re-enabling the @PreAuthorize annotation seems to introduce the improper behavior and skips the breakpoint. Where in the surrounding Spring infrastructure could I place the breakpoint to research deeper this effect of @PreAuthorize("hasRole('ADMIN')") annotation?

Thanks.

Upvotes: 0

Views: 913

Answers (1)

Dzinot
Dzinot

Reputation: 549

Because you are using OAuth2, instead of using the WebSecurityConfigurerAdapter you need to use the ResourceServerConfigurerAdapter. For example:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().permitAll().and().cors().disable().csrf().disable().httpBasic().disable()
            .exceptionHandling()
            .authenticationEntryPoint(
                    (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .accessDeniedHandler(
                    (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED));
  }
}

Now you should be able to use the security methods.

Here is an implementation for OAuth2 with Spring Boot with custom UserDetails service and JWT token. The basic implementation is the same.

Here is the OAuth2Configuration for the Authorization Server

Here is the WebSecurityConfiguration for the Authorization Server

Upvotes: 1

Related Questions