Coder Social home page Coder Social logo

Using annotations in a cucumber test environment with MockMvcSupport does not create a Principal in Spring Boot about spring-addons HOT 27 CLOSED

ch4mpy avatar ch4mpy commented on June 14, 2024
Using annotations in a cucumber test environment with MockMvcSupport does not create a Principal in Spring Boot

from spring-addons.

Comments (27)

ch4mpy avatar ch4mpy commented on June 14, 2024 1

It is in your runtime config, but I can't see your WebSecurityConfig referenced in test configuration you provided.

If you share a small project, I might add it to samples in tests once we have it working.

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024 1

don't bother with the sample. I built one and figured out why security is not working: WithSecurityContextTestExecutionListener is not triggered by Cucumber JUnit test runner.

I oppened a ticket on Cucumber-jvm: cucumber/cucumber-jvm#2408

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

Hi,

Do you have cucumber-spring on your test classpath?
Did you check articles like https://www.baeldung.com/cucumber-spring-integration?

I think what you encounter is not a bug from this lib but rather a compatibility issue between Spring and Cucumber.

You need a spring context (with a spring security-context) for @WithMock... to work (for the dots, put User, KeycloakAuth, JwtAuth or whichever test annotation itslef decorated with @WithSecurityContext): your test must run with a JUnit spring runner or extension, something like @WebMvcTest, @SpringBootTest, etc.

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

Hi,
Yes we already have hundreds of cucumber tests running with Spring. But this is the first bunch tests that I want to run against spring security configurations. I think I will switch to junit testing and give your lib a try outside cucumber. Will let you know.
Thanks

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

Hi,
As expected, your lib is working in a pure JUnit MockMvc context. It would nice to understand why it is not working in a cucumber MockMVC context. Do you know why the Principal parameter in the controller method is not instanciated as it is at runtime? Here's a snippet and would expect the Principal parameter to not be null.
Thanks

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping("")
    public List<ProductLightDTO> searchProducts(@RequestBody ProductFilter productFilter, Principal principal) {
        return productService.search(productFilter);
    }

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

Hi @eaudet ,

I got a look at Cucumber and here is what I understood. Please correct me if I'm wrong:

When running with Cucumber, REST service is launched separatly from steps
=> tested code (REST api) and testing code (Cucumber steps) run in different processes
=> @WithSecurityContext annotations will be of no help, you need actual (valid) JWT bearer tokens in authorisation header of your requests

This lib (and spring-security test framework) are designed to run unit tests, not integration: it is designed to shortcut JWT decoding, validation etc. and inject a mocked security context in current thread (no encoded JSON web token is created).

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

Hi @ch4mpy ,
Steps and Spring components run in the same process/thread. Steps can autowire a spring component. Hence, you can test a service directly without calling a REST controller. You can also use SpringMVC (MockMVC) for testing rest endpoints. You can see the thread id is the same in service and cucumber steps. So I guess your wrong. We're not doing integration tests here but really unit tests. The KeycloakAuthenticationProvider.class is not even called during junit test and cucumber test. So your lib must be able to inject the KC token or a basic auth into spring context...
Erick

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

If so, yes, it should be possible to us @WithMockKeycloakAuth to build a KeycloakAuthenticationToken and inject it in test security context.

You might have a test configuration issue. Most probably @EnableWebSecurity and @EnableGlobalMethodSecurity are not included.

I'd be interested in a sample spring-boot project with Cucumber for unit-tests. Ideally working for non secured REST endpoints.

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

Both @EnableWebSecurity and @EnableGlobalMethodSecurity are included as I wrote in my original issue. I will try to find a moment to build a project.

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

Hi, I have jump to cucumber 7.0.0 (as mentionned) but still have the same error. The annotation @WithMockKeycloakAuth({"ROLE_user"}) is not injected in spring context. Here's the stack trace:

Step failed
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:652)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:77)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
	at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
	at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
	at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:183)
	at com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport.perform(MockMvcSupport.java:154)

from spring-addons.

mpkorstanje avatar mpkorstanje commented on June 14, 2024

Keep in mind that:

  1. WithSecurityContextTestExecutionListener looks for annotations on test methods and if not found there on the test class.

https://github.com/spring-projects/spring-security/blob/c82722c412efac57fd89c0158ae82e45525cfd2e/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java#L64-L70

  1. Cucumber does not have test methods. So I don't think this will work (but I don't know how WithMockKeycloakAuth is supposed to work):
@WithMockKeycloakAuth({ "ROLE_user" })
    public List<ProductLightDTO> securedProductSearch(ProductFilter filter, String username, String password) throws Exception {

So you may have to put the @WithMockKeycloakAuth annotation on the class annotated with @CucumberContextConfiguration.

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

@mpkorstanje in order to make this @WithMockKeycloakAuth useful, it needs to be on the test method and not global on the class annotated @CucumberContextConfiguration. Otherwise you cannot test finegrained role base access to spring services.

I found something interesting by adding a RequestPostProcessor including a principal and a role in the mockMvc. This actually injects a principal in spring security context and I am able to have a fully instantiated Principal object in my spring service. But the role is still not injected correctly. Maybe this can help?

MvcResult result = mockMvc.perform(post("/products").contentType(APPLICATION_JSON_UTF8)
                .content(body).with(authUtil.getRequestPostProcessor("fakeOwner", "ROLE_user"))).andReturn();

My authUtil class:

@Component
public class AuthUtil {

    public RequestPostProcessor getRequestPostProcessor(String ownerUuid, String userRole) {
        return mockHttpServletRequest -> {
            mockHttpServletRequest.setUserPrincipal(() -> ownerUuid);
            mockHttpServletRequest.addUserRole(userRole);
            return mockHttpServletRequest;

        };
    }
}

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

@mpkorstanje

  1. @WithMockKeycloakAuth is decorated with @WithSecurityContext. It works just as @WithMockUser: WithSecurityContextTestExecutionListener calls a factory to get an Authentication instance and then configures test SecurityContext with it.

  2. I know WithSecurityContextTestExecutionListener is a Spring construct made to work with JUnit and that it does not work with Cucumber out of the box, reason for creating the ticket on cucumber-jvm which contains a cucumber-spring module (which I understand as being intended to make a bridge with Spring JUnit tests)

I'm very new to Cucumber, but maybe could it be possible to either

  • define @Given steps which trigger WithSecurityContextTestExecutionListener logic
  • hook into @When steps also decorated with @WithSecurityContext annotations (@WithMockUser, @WithMockKeycloakAuth or whatever) to trigger WithSecurityContextTestExecutionListener logic

Maybe second option is more flexible but first is more "Cucumber oriented"?

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

@eaudet I have a workaround for you untill we find a way to have annotations working: use MockMvc request post processor.

I just pushed a demo of that on gh-29 branch:

public class GreetingControllerSuite {

	@Autowired
	MockMvc mockMvc;

	@Autowired
	ServletKeycloakAuthUnitTestingSupport keycloak;

	MvcResult result;

	@When("authenticated users want to get greeting")
	public void authenticatedUsersWantToGetGreeting() throws Exception {
		result = mockMvc.perform(get("/greet").with(keycloak.authentication())).andReturn();
	}

	@Then("a greeting is returned")
	public void aGreetingIsReturned() throws Exception {
		assertThat(result.getResponse().getContentAsString()).contains("Hello user");
	}

}

You can of course chain many method calls after .with(keycloak.authentication()) to custumize KeycloakAuthenticationToken (authorities, OpenID claims, etc.)

from spring-addons.

mpkorstanje avatar mpkorstanje commented on June 14, 2024

It's not quite obvious but a cucumber scenario matches to a junit method. As if calling several step definition methods from a single test method. This means that if the JUnit method does something to setup authorization this must be reflected in the steps of the scenario. So the annotation is essentially an extra method call.

This means that your workaround is infact the correct solution.

from spring-addons.

mpkorstanje avatar mpkorstanje commented on June 14, 2024

It's also not possible to trigger any TestExecutionListener on a specific step definition method. The listeners are part of Springs Test Context manager framework and the expectation is that the listeners are invoked around a test. This doesn't match the scope of step definition methods which are strictly inside a test.

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

TestSecurityContext being configured before the test or as first step inside the test would not make much difference.

from spring-addons.

mpkorstanje avatar mpkorstanje commented on June 14, 2024

Not in this instance but remember we're managing this through an abstraction. The conceptual mismatch isn't something that can be resolved for all possible cases.

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

@mpkorstanje what do you suggest to test secured @Service with Cucumber?
MockMvc request post processors can not be used in that case and Spring security-context must be configured.

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

@ch4mpy in my link above (#29 (comment)) I can inject a postProcessor and get a Principal in my @service methods....if only I could get the roles in the same way. This seems like a MockMVC bug to me.

from spring-addons.

mpkorstanje avatar mpkorstanje commented on June 14, 2024

what do you suggest to test secured @service with Cucumber?

Haven't tried but WithSecurityContextTestExecutionListener ultimately calls TestSecurityContextHolder.setContext(SecurityContext) and TestSecurityContextHolder.clearContext(). I believe you can do that in a step definition manually.

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

That's what I'm working on.
It's pretty verbose and inconvenient.

from spring-addons.

mpkorstanje avatar mpkorstanje commented on June 14, 2024

You can get it pretty lean I think.

withMockKeycloakAuth().authorities("USER").claims(OpenIdClaims.preferredUsername("ch4mpy"));

Of course you'd have to write the DSL for that but that's a one time thing.

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

I got the following suite pass:

Feature: Testing a secured REST API
  Authenticated users should be able to GET greetings

  Scenario: Authorized users should be greeted
    Given the following user roles:
      | ROLE_user   |
      | ROLE_TESTER |
    When a get request is sent to greeting endpoint
    Then a greeting is returned

  Scenario: Unauthorized users shouldn't be greeted
    Given user is not authenticated
    When a get request is sent to greeting endpoint
    Then user is redirected to login

With this steps class:

package com.c4_soft.springaddons.samples.webmvc.keycloak.cucumber.steps;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

import java.util.List;
import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import com.c4_soft.springaddons.security.oauth2.test.mockmvc.keycloak.ServletKeycloakAuthUnitTestingSupport;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

public class GreetingControllerSuite {

	@Autowired
	MockMvc mockMvc;

	@Autowired
	ServletKeycloakAuthUnitTestingSupport keycloak;

	MvcResult result;

	@Given("user is not authenticated")
	public void unauthenticatedUser() {
		TestSecurityContextHolder.clearContext();
	}

	@Given("the following user roles:")
	public void authenticateAsUser(List<String> rolesTable) {
		TestSecurityContextHolder.clearContext();
		final Stream<String> roles = rolesTable.stream().map(String::trim);
		TestSecurityContextHolder.setAuthentication(keycloak.authentication().authorities(roles).build());
	}

	@When("a get request is sent to greeting endpoint")
	public void getGreet() throws Exception {
		result = mockMvc.perform(get("/greet")).andReturn();
	}

	@Then("user is redirected to login")
	public void redirectedToLogin() throws Exception {
		assertEquals(302, result.getResponse().getStatus());
	}

	@Then("a greeting is returned")
	public void greetingIsReturned() throws Exception {
		assertEquals(200, result.getResponse().getStatus());
		final String body = result.getResponse().getContentAsString();
		assertThat(body.contains("Hello user! You are granted with "));
		assertThat(body.contains("ROLE_user"));
		assertThat(body.contains("ROLE_TESTER"));
	}

}

I'm sorry, but I can't get annotations working with Cucumber for the reasons mentioned above.

I hope the solution in this message is acceptable enough. If so, would you kindly close the ticket @eaudet?

from spring-addons.

mpkorstanje avatar mpkorstanje commented on June 14, 2024

The input from the table is already trimmed. You can remove that line.

from spring-addons.

eaudet avatar eaudet commented on June 14, 2024

@ch4mpy and @mpkorstanje thanks for the analysis. Even if annotations are not available, using roles in the Scenario directly is even better. Thanks!

from spring-addons.

ch4mpy avatar ch4mpy commented on June 14, 2024

Added a Cucumber.md to master branch

from spring-addons.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.