2

I am new to Spring Boot and Spring Security and have inherited a webapp project that uses them. We will be migrating the webapp to a new deployment environment. One of the things we will be changing is the authentication mechanism, so that it will operate in the new environment. Meanwhile, I'd like use some existing PostMan tests to exercise the REST endpoints, bypassing security. Basically, I want to disable security temporarily.

I have a class that provides global method level security:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    // ...
}

I have multiple controller classes, e.g., a class that provides information about users:

@RestController
public class UserController {

    @ApiOperation(value = "Gets the list of users for an admin.")
    @PreAuthorize("#oauth2.hasScope('dashboard') and #oauth2.hasScope('read') and hasRole('ROLE_SYSADMIN')")
    @GetMapping(value = "/user/list", produces = MediaType.APPLICATION_JSON_VALUE)
    @AuditAccess(message = "Accessing /user/list")
    public ResponseEntity<List<UserResponse>> getUserList() {
        // ...
    }

    // ...
}

If I try to run my PostMan tests, they fail, because there is no authentication mechanism set up in my local test environment. My first attempt to bypass security was to comment out the @EnableGlobalMethodSecurity line, but that led to run-time errors, presumably due to the @PreAuthorize annotations on the methods. If I comment out those also, I can get the tests to run. However, there are many such annotations spread across many files. I'd rather not comment out all that; I'd rather temporarily substitute a "do nothing" version of method security. Here's my attempt:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    /**
     * This class is designed to let everything pass the method filter, i.e.,
     * effectively removing method level security.
     */
    class DoNothingMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

        public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx) {
            return filterTarget;
        }

        @Override
        public void setReturnObject(Object returnObject, EvaluationContext ctx) {
            // do nothing
        }
    }

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        MethodSecurityExpressionHandler doNothingHandler =
                new DoNothingMethodSecurityExpressionHandler();
        return doNothingHandler;
// TODO restore below
//        return new OAuth2MethodSecurityExpressionHandler();
    }
}

If I try to run my PostMan test with

GET http://localhost:8080/myapp/user/list

I get an error:

2021-12-21 06:28:14,252 INFO  [stdout] (default task-1) Request: http://localhost:8080/myapp/user/list }raised
2021-12-21 06:28:14,253 INFO  [stdout] (default task-1) org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
2021-12-21 06:28:14,254 INFO  [stdout] (default task-1)     at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:379) ~[spring-security-core-5.3.1.RELEASE.jar:5.3.1.RELEASE]
2021-12-21 06:28:14,254 INFO  [stdout] (default task-1)     at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:223) ~[spring-security-core-5.3.1.RELEASE.jar:5.3.1.RELEASE]
2021-12-21 06:28:14,254 INFO  [stdout] (default task-1)     at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65) ~[spring-security-core-5.3.1.RELEASE.jar:5.3.1.RELEASE]
2021-12-21 06:28:14,255 INFO  [stdout] (default task-1)     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,255 INFO  [stdout] (default task-1)     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,256 INFO  [stdout] (default task-1)     at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,257 INFO  [stdout] (default task-1)     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,257 INFO  [stdout] (default task-1)     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,257 INFO  [stdout] (default task-1)     at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,258 INFO  [stdout] (default task-1)     at mycompany.rest.controller.UserController$$EnhancerBySpringCGLIB$$f2b6b566.getUserList(<generated>) ~[classes:?]
2021-12-21 06:28:14,259 INFO  [stdout] (default task-1)     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_271]
2021-12-21 06:28:14,259 INFO  [stdout] (default task-1)     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_271]
2021-12-21 06:28:14,260 INFO  [stdout] (default task-1)     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_271]
2021-12-21 06:28:14,260 INFO  [stdout] (default task-1)     at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_271]
2021-12-21 06:28:14,260 INFO  [stdout] (default task-1)     at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,261 INFO  [stdout] (default task-1)     at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,261 INFO  [stdout] (default task-1)     at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,263 INFO  [stdout] (default task-1)     at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,264 INFO  [stdout] (default task-1)     at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,265 INFO  [stdout] (default task-1)     at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,265 INFO  [stdout] (default task-1)     at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]
2021-12-21 06:28:14,266 INFO  [stdout] (default task-1)     at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.5.RELEASE.jar:5.2.5.RELEASE]

For some reason, we are still trying to authenticate.

This question is similar to some others (Spring Boot 2.0 Disable Default Security, Spring Oauth2 : Authentication Object was not found in the SecurityContext, and An Authentication object was not found in the SecurityContext - Spring 3.2.2), but involves different versions and a different use case. I haven't been able to figure out how to apply those answers to my situation. My Spring environment includes:

<spring.version>5.2.5.RELEASE</spring.version>
<spring.oath2.version>2.4.1.RELEASE</spring.oath2.version>
<spring.boot.version>2.2.6.RELEASE</spring.boot.version>
<spring.jwt.version>1.1.0.RELEASE</spring.jwt.version>
<spring.security.version>5.3.1.RELEASE</spring.security.version>

What would be an easy way to bypass security?

kc2001
  • 5,008
  • 4
  • 51
  • 92
  • I would first like to know why you are using multiple different versions of spring, spring is a ecosystem with versions that work together, spring-boot pulls in the spring context automatically, and spring security already comes with nimbus jwt library, so i would first like to know the reasoning for the strange dependency usage – Toerktumlare Dec 27 '21 at 11:58
  • If this is a spring boot application, you should be using the spring boot parent or bom, and then use spring boot starters to let spring handle the versions for you. Why im pointing this out is that i dont want to give answers to something that ”might not work” because of the chaotic dependencies – Toerktumlare Dec 27 '21 at 12:00
  • @Toerktumlare - I don't know why the versions are inconsistent; these are the version numbers I inherited. I will try to clean them up. – kc2001 Dec 27 '21 at 14:54
  • 1
    @kc2001 Do you have access to the actual application running? At least, are you able to login against the service used to authenticate users in your application? If so, try debugging the requests interchanged in the auth flow, perhaps between your frontend and backend, and try obtaining the JWT token returned after successful authentication, the one you are using to authenticate your frontend requests. Then, use that token in your Postman requests passing it as an `Authorization: Bearer` token header: it will depend on your actual configuration, but it may work. – jccampanero Dec 27 '21 at 17:04
  • 1
    When the token expires or when you need to test with another profile, login in the actual application again with the appropriate user, and repeat the process. If you wish, you can configure a variable in Postman to hold the current JWT token value that should be used in every case, and use that variable in your tests when constructing the `Authentication` header. – jccampanero Dec 27 '21 at 17:06

3 Answers3

3

"Switching" code (un-/commenting) for tests is a NO-GO! (In "strict teams" you may not even commit commented code! -> sonarqube)

Where in complex (distributed) environments, having (spring) @Profile("test")(e.g. switching off security), sounds absolutely legitime!

Still it is [very good - essential] to (unit+integration) test also your security (+configuration)!

Spring Security 5.5.x Reference Documentation, Chapter 19 Testing

This section describes the testing support provided by Spring Security.

19.1. Testing Method Security

Before we can use Spring Security Test support, we must perform some setup. An example can be seen below:

@RunWith(SpringJUnit4ClassRunner.class) // 1.
@ContextConfiguration // 2.
public class WithMockUserTests { ...

(We can have both in one: @SpringBootTest ;)

This is a basic example of how to setup Spring Security Test. The highlights are:

  1. @RunWith instructs the spring-test module that it should create an ApplicationContext. This is no different than using the existing Spring Test support. For additional information, refer to the Spring Reference
  2. @ContextConfiguration instructs the spring-test the configuration to use to create the ApplicationContext. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the Spring Reference

Spring Security hooks into Spring Test support using the WithSecurityContextTestExecutionListener which will ensure our tests are ran with the correct user. It does this by populating the SecurityContextHolder prior to running our tests. If you are using reactive method security, you will also need ReactorContextTestExecutionListener which populates ReactiveSecurityContextHolder. After the test is done, it will clear out the SecurityContextHolder. If you only need Spring Security related support, you can replace @ContextConfiguration with @SecurityTestExecutionListeners.

Remember we added the @PreAuthorize annotation to our HelloMessageService and so it requires an authenticated user to invoke it. If we ran the following test, we would expect the following test will pass:

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
   messageService.getMessage();
}

(Tests expected (authentication) exception!), so one of:

19.1.2. @WithMockUser

...

19.1.3. @WithAnonymousUser

...

19.1.4. @WithUserDetails

...

19.1.5. @WithSecurityContext

...

19.1.6. Test Meta Annotations

... and the rest of the chapter/reference, will be useful.

xerx593
  • 12,237
  • 5
  • 33
  • 64
  • 1
    I support this answer, tests should be run with security enabled. In this case where there is postman tests, den a profile would be most suitable. But the tests should be rewritten from postman to spring boot as quick as possible. – Toerktumlare Dec 27 '21 at 15:12
2

Instead of removing @EnableGlobalMethodSecurity , you can keep it but just disable its prePostEnabled.

But it requires to have at least of one the following is enabled (see this for the details) :

  • prePostEnabled (For enable @PreAuthorize / @PreFilter / @PostAuthorize / @PostFilter)
  • securedEnabled (For enable @Secured)
  • jsr250Enabled (For enable JSR-250 annotations such as @DenyAll , @PermitAll , @RolesAllowed)
  • define a custom MethodSecurityMetadataSource

Since you only enable prePostEnabled in your existing configuration , I believe you should not use any @Secured and JSR-250 annotation on any methods now.

So the following trick should just disable @PreAuthorize only while keeping other things remain unchanged :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = false, securedEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

}

or

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = false, jsr250Enabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

}
Ken Chan
  • 84,777
  • 26
  • 143
  • 172
1

You can try setting prePostEnabled = false and then removing any authentication filters in WebSecurityConfigurerAdapter implementation with something like

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable().authorizeRequests()
                .anyRequest().permitAll();

    }
sshivampp
  • 11
  • 2