Skip to content

formLogin() does not work with REST Docs #7572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ValtteriL opened this issue Oct 26, 2019 · 10 comments
Closed

formLogin() does not work with REST Docs #7572

ValtteriL opened this issue Oct 26, 2019 · 10 comments
Assignees
Labels
in: core An issue in spring-security-core status: backported An issue that has been backported to maintenance branches type: enhancement A general enhancement
Milestone

Comments

@ValtteriL
Copy link

Summary

I was asked to open this issue here after Spring REST Docs team investigated my issue why REST Docs did not get configured with formLogin().

SecurityMockMvcRequestBuilders$FormLoginRequestBuilder does not implement Mergeable so the default configuration that is set up on the MockMvc instance is not applied. Crucially in the case of Spring REST Docs, this means that the ConfigurerApplyingRequestPostProcessor that applies the REST Docs context to the request is lost.

The original issue can be found here: spring-projects/spring-restdocs#655

The following code causes java.lang.IllegalStateException: REST Docs configuration not found. Did you forget to apply a MockMvcRestDocumentationConfigurer when building the MockMvc instance?

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
public class LoginLogoutTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void adminCanLoginLogout() throws Exception {
        mockMvc.perform(formLogin().user(TestConfig.ADMIN_USERNAME).password(TestConfig.PASSWORD))
            .andExpect(status().isOk())
            .andExpect(authenticated().withUsername(TestConfig.ADMIN_USERNAME))
            .andDo(document("login"));

        mockMvc.perform(logout())
            .andExpect(status().isOk())
            .andExpect(unauthenticated())
            .andDo(document("logout"));
    }
}

Actual Behavior

On mvn test the code causes java.lang.IllegalStateException: REST Docs configuration not found. Did you forget to apply a MockMvcRestDocumentationConfigurer when building the MockMvc instance? on the line with document("login").

Expected Behavior

No errors. The REST Docs should be correctly configured.

Version

All versions

Sample

restdocs-formlogin.zip
This sample causes the same error on mvn test.

@rhamedy
Copy link
Contributor

rhamedy commented Oct 27, 2019

Out of curiosity and the following comment

SecurityMockMvcRequestBuilders$FormLoginRequestBuilder does not implement Mergeable so the default configuration that is set up on the MockMvc instance is not applied. Crucially in the case of Spring REST Docs, this means that the ConfigurerApplyingRequestPostProcessor that applies the REST Docs context to the request is lost.

I decided to give this a try. I updated the SecurityMockMvcRequestBuilders$FormLoginRequestBuilder to implement the Mergeable interface. I wrote a test in SecurityMockMvcRequestBuildersFormLoginTests and the test seems to work as expected and perform the merge

@Test
public void mergeWorksAsExpected() throws Exception {
	String username = "userA";
	String password = "passwordA";
	String loginUrl = "/signin";

	SecurityMockMvcRequestBuilders.FormLoginRequestBuilder builder = formLogin()
			.user(" ")
			.password(" ")
			.loginProcessingUrl(" ");

	SecurityMockMvcRequestBuilders.FormLoginRequestBuilder defaultBuilder = formLogin()
			.loginProcessingUrl(loginUrl)
			.user(username)
			.password(password);

	builder.merge(defaultBuilder);

	MockHttpServletRequest request = builder.buildRequest(servletContext);

	assertThat(request.getParameter("username")).isEqualTo(username);
	assertThat(request.getParameter("password")).isEqualTo(password);
	assertThat(request.getPathInfo()).isEqualTo(loginUrl);

}

I then exported the spring-security-test project as jar from eclipse and installed it in the provided sample project by ValtteriL using a local maven-repository. I run the tests but, still throwing the same exception

java.lang.IllegalArgumentException: Cannot merge with [org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder]
	at com.service.backend.login.LoginLogoutTest.adminCanLoginLogout(LoginLogoutTest.java:33)

Curious why the merge method is not invoked 🤔 Either more changes in addition to merge and is merge enabled is required or I did not correctly configure the sample project to use the updated spring-security-test jar.

@fhanik fhanik added status: ideal-for-contribution An issue that we actively are looking for someone to help us with type: enhancement A general enhancement in: core An issue in spring-security-core and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 28, 2019
@fhanik fhanik added this to the 5.2.1 milestone Oct 28, 2019
@rwinch rwinch modified the milestones: 5.2.1, 5.2.2 Nov 4, 2019
@sandmannn
Copy link
Contributor

sandmannn commented Dec 8, 2019

Looks like there are two additional components to this change, one in spring-framework and another in spring-security.

Firstly, we need to read the post processors from the parent during the merge() call and save them in the instance of the FormLoginRequestBuilder. We need to create an additional interface that has the capability to provide its internally stored postProcessorts via getPostProcessors() method. I am not sure what a good name can be here, e.g. MergeableParentSmartRequestBuilder. There is already an interface ConfigurableSmartRequestBuilder in org.springframework.test.web.servlet.request that allows adding to a class an additional post processor using the with() method; we want to have an additional interface to get all the stored post processors.
MockHttpServletRequestBuilder should implement this interface. In the newly created merge() call of the FormLoginRequestBuilder we need to check if the parent argument of the merge() call belongs to this new interface, retreive the postProcessors using the getter function from the parent (MockHttpServletRequestBuilder will be the most important implementor of the interface) and save them in the FormLoginRequestBuilder.

Secondly, we need to implement the SmartRequestBuilder in the FormLoginRequestBuilder. This allows executing the postProcessRequest() method which is the way for the required documentation-related settings to be passed along.

Thus it looks like there are prerequisites that must be applied in the spring-framework repository first https://github.com/spring-projects/spring-framework/blob/master/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java before proceeding with this issue (both the new interface and implementation of this interface in the MockHttpServletRequestBuilder. Adding the contributors to relevant classes @rstoyanchev and @rwinch , please comment if we should proceed in this way.

@jgrandja jgrandja modified the milestones: 5.2.2, 5.2.3 Feb 5, 2020
@eleftherias eleftherias modified the milestones: 5.2.3, 5.2.4 Apr 1, 2020
@dadikovi
Copy link
Contributor

dadikovi commented May 1, 2020

Hi! Is there any update about this issue? Is there a corresponding issue in the spring-framework repository as well? Is this issue still considered ideal for contribution, because if yes I would like to take it :)

@rwinch
Copy link
Member

rwinch commented May 1, 2020

@dadikovi The issue is yours.

NOTE: I'm not aware of any corresponding issues logged in Spring Framework.

@dadikovi
Copy link
Contributor

dadikovi commented May 2, 2020

@rwinch but the solution defined in #7572 (comment) seems legit?

@rwinch
Copy link
Member

rwinch commented May 5, 2020

@dadikovi I haven't looked into the proposed solution. What might be a good place to start is producing a unit test that reproduces the issue so I can easily look at it in isolation. You could do this with a draft PR. What do you think?

@eleftherias eleftherias modified the milestones: 5.2.4, 5.2.5 May 6, 2020
@dadikovi
Copy link
Contributor

dadikovi commented May 9, 2020

@rwinch Thank you for your answer, I will do that! :)

dadikovi added a commit to dadikovi/spring-security that referenced this issue May 9, 2020
This commit adds a new test which fails right now, and also suggest a change.

Fixes spring-projectsgh-7572
@dadikovi
Copy link
Contributor

dadikovi commented May 9, 2020

@rwinch I could manage to understand the problem here, @sandmannn 's comment #7572 (comment) seems 100% accurate for me, but since the solution would need contribution in another spring project (which I would do happily) and I'm new here, I would appreciate your double-check.
I will provide further details in commit under the PR.

@rwinch
Copy link
Member

rwinch commented May 12, 2020

@dadikovi I don't think anything needs to be done outside of Spring Security. I believe if these changes are made to Spring Security's FormLoginRequestBuilder everything work work correctly:

private Mergeable mergeable;

@Override
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
	RequestBuilder builder = post(this.loginProcessingUrl)
			.accept(this.acceptMediaType).param(this.usernameParam, this.username)
			.param(this.passwordParam, this.password);
	if (this.mergeable != null) {
		builder = (RequestBuilder) this.mergeable.merge(builder);
	}
	MockHttpServletRequest request = builder
			.buildRequest(servletContext);
	return this.postProcessor.postProcessRequest(request);
}

@Override
public Object merge(Object o) {
	this.mergeable = (Mergeable) o;
	return this.mergeable;
}

Similar changes would need to be applied to LogoutRequestBuilder. Would you be interested in trying to verify this would work and if so putting a PR together based on this work?

@dadikovi
Copy link
Contributor

@rwinch wow, it works! Thanks!
My suspicion is, that this expects that the merge method of parent will copy all of the headers, params and the URL from the FormLoginRequestBuilder instance.
Probably I'm overthinking it, but I found some places (eg. in MockMultipartHttpServletRequestBuilder) a check if the parent is instance of MockHttpServletRequestBuilder (this implementation does the necessary copies), and an IllegalArgumentException throw otherwise. Should we do this?
Anyway, I will send these changes to the PR, also, I added the same changes to logout as well.
If anything more (or less) needed for this PR, please let me know :)

rwinch pushed a commit to rwinch/spring-security that referenced this issue May 27, 2020
Right now formLogin() does not work with REST Docs.

This commit adds a new test which fails right now, and also suggest a change.

Fixes spring-projectsgh-7572
@eleftherias eleftherias modified the milestones: 5.2.5, 5.2.6 Jun 3, 2020
@rwinch rwinch removed the status: ideal-for-contribution An issue that we actively are looking for someone to help us with label Jun 22, 2020
@rwinch rwinch closed this as completed in 88028d8 Jun 22, 2020
rwinch pushed a commit that referenced this issue Jun 22, 2020
This is necessary so that default requests like Spring REST Docs work.

Closes gh-7572
@spring-projects-issues spring-projects-issues added the status: backported An issue that has been backported to maintenance branches label Jun 22, 2020
@rwinch rwinch modified the milestones: 5.2.6, 5.4.0-M2 Jun 22, 2020
rwinch pushed a commit that referenced this issue Jun 22, 2020
This is necessary so that default requests like Spring REST Docs work.

Closes gh-7572
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core An issue in spring-security-core status: backported An issue that has been backported to maintenance branches type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants