Description
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();
}