Skip to content

HttpSessionSecurityContextRepository fails to create a session because of the deferred security context support #12314

Closed
@sbegaudeau

Description

@sbegaudeau

Describe the bug

Short version

The new support for deferred security context makes DelegatingSecurityContextRepository#loadContext call HttpSessionSecurityContextRepository#loadDeferredContext while HttpSessionSecurityContextRepository#loadContext would have been called before. The response wrapper is thus not configured by HttpSessionSecurityContextRepository#loadContext when the context is loaded but only when the context is to be saved. As a result, when that wrapper is finally created, the response is now committed, it did not have the ability to act as an OnCommittedResponseWrapper and it can't create a session anymore, it's too late.

Long version

I am using the oauth2Login() in my security configuration and there seems to be a change in behavior caused by this commit introducing the support for deferred SecurityContext which makes my application throw an exception with Spring Security 6.0.0 while it worked with Spring Security 5.7.3.

I am using the OAuth2 support to log in with Github and everything works fine with regard to the communication with Github but by migrating to Spring Security 6.0.0, after the authentication, new requests cannot find the security context and return a 401.

I have thus used http.securityContext((securityContext) -> securityContext.requireExplicitSave(false)); to restore the existing behavior of Spring Security 5.7 but now the code is producing the following error:

w.c.HttpSessionSecurityContextRepository : Failed to create a session, as response has been committed. Unable to store SecurityContext.

From what I have understood, in my application with Spring Security 5.7.3, the SecurityContextPersistenceFilter used the HttpSessionSecurityContextRepository and the call to SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder); would thus call HttpSessionSecurityContextRepository#loadContext. In that method, a wrapper was added to both the request and the response:

SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request, httpSession != null, context);
wrappedResponse.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));

As a result, when the success handler of my OAuth2 login was called, it could perform the redirection with:

this.getRedirectStrategy().sendRedirect(request, response, redirectUri);

Now the problem is that, this call to sendRedirect is executed by an OnCommittedResponseWrapper, which is the HeaderWriterResponse (as before) and underneath I don't have the SaveToSessionResponseWrapper anymore. In Spring Security 5.7, this wrapper would save the context by executing the following code before the response was committed:

@Override
protected void onResponseCommitted() {
	saveContext(this.securityContextHolderStrategy.getContext());
	this.contextSaved = true;
}

Now instead, the call to HttpSessionSecurityContextRepository#loadDeferredContext caused by DelegatingSecurityContextRepository#loadContext does not create this wrapper anymore. A wrapper is only created later thanks to the call to:

this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());

By the SecurityContextPersistenceFilter after the response has been committed. Now the method DelegatingSecurityContextRepository#saveContext will call HttpSessionSecurityContextRepository#saveContext which will create a new response wrapper but long after the response has been committed:

public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
	SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, SaveContextOnUpdateOrErrorResponseWrapper.class);
	if (responseWrapper == null) {
		boolean httpSessionExists = request.getSession(false) != null;
		SecurityContext initialContext = this.securityContextHolderStrategy.createEmptyContext();
		responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
	}
	responseWrapper.saveContext(context);
}

When this new instance of the response wrapper will try to do its job, the response would have been committed already and the exception would appear. As a result, the authentication does not work.

Expected behavior
The use of HttpSessionSecurityContextRepository should create the same wrapper as before to save the security context in the session before the response is committed. I don't know if HttpSessionSecurityContextRepository#loadDeferredContext can do it since we don't have access to the response since it is "lost" by the DelegatingSecurityContextRepository in:

@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
	return loadDeferredContext(requestResponseHolder.getRequest()).get();
}

Metadata

Metadata

Labels

in: webAn issue in web modules (web, webmvc)type: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions