Description
Expected Behavior
WebSessionServerRequestCache
or another ServerRequestCache
implementation should support saving POST
requests for replaying after authentication is successful.
Current Behavior
WebSessionServerRequestCache
only supports saving GET
requests. It can be extended to support matching other requests with setSaveRequestMatcher()
but it will only be able to save/replay the original GET
url.
Context
We have internally implemented SAML in spring security by creating my own AuthenticationWebFilter
that is configured to create and validate SAML requests and responses. Our SAML supports configuration as both an IDP
and SP
. The SAML core is implemented around org.opensaml
XML and security api's. This is as requested in issue #7954
Background reading on SAML: http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html
See section: SP-Initiated SSO: Redirect/POST Bindings
When our server is configured as an IDP, a POST request may come to our server with a request for SAML verification. Our IDP endpoint looks like this:
@RestController
@RequestMapping("/saml/idp")
public class SamlIdpController {
@PostMapping(value = "/ls", produces = MediaType.TEXT_HTML_VALUE)
public Mono<String> loginService(Principal principal, ServerWebExchange exchange) {
return exchange.getFormData().flatMap(formData -> {
String samlRequest = formData.getFirst("SAMLRequest");
String relayState = formData.getFirst("RelayState");
if (samlRequest != null) {
return processLoginRequest("POST", principal, samlRequest, relayState);
}
logger.info("Authentication did not succeed for SAMLRequest: {} from {}", samlRequest, getRemoteHostAddress(exchange.getRequest()));
return Mono.empty();
});
}
}
Before the POST can be processed the authentication is handled with AuthenticationWebFilter
. If the users is not authenticated they will be redirected for authentication. i.e. LoginForm/BasicAuth. Then they "should" be redirected back to the SAML IDP endpoint.
This doesn't doesn't happen because the WebSessionServerRequestCache
wired into AuthenticationWebFilter
doesn't support replaying the SAML POST request.
Workaround
I have implemented a PostSavingWebSessionRequestCache
. The does something like the below, and with another hack it works:
public class PostSavingWebSessionRequestCache implements ServerRequestCache {
@Override
public Mono<Void> saveRequest(ServerWebExchange exchange) {
String requestPath = pathInApplication(exchange.getRequest());
if (exchange.getRequest().getMethod() == POST) {
return this.postMatcher.matches(exchange).filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap((m) -> exchange.getSession()).map(WebSession::getAttributes).doOnNext((attrs) -> {
attrs.put(SAVED_PATH_ATTR, requestPath);
attrs.put(SAVED_REQUEST_ATTR, serializePost(exchange));
logger.info("POST Request added to WebSession: {}", requestPath);
}).then();
} else {
return this.getMatcher.matches(exchange).filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap((m) -> exchange.getSession()).map(WebSession::getAttributes).doOnNext((attrs) -> {
attrs.put(SAVED_PATH_ATTR, requestPath);
logger.info("{} Request added to WebSession: {}", exchange.getRequest().getMethod(),
requestPath);
}).then();
}
}
@Override
public Mono<ServerHttpRequest> removeMatchingRequest(ServerWebExchange exchange) {
return exchange.getSession().map(WebSession::getAttributes).flatMap((attributes) -> {
String requestPath = pathInApplication(exchange.getRequest());
boolean removed = attributes.remove(SAVED_PATH_ATTR, requestPath);
if (removed) {
logger.debug(LogMessage.format("Request removed from WebSession: '%s'", requestPath));
}
if (removed) {
ServerHttpRequest request = (ServerHttpRequest) attributes.remove(SAVED_REQUEST_ATTR);
if (request == null) {
logger.info("{} Request continuing: {}", exchange.getRequest().getMethod(), requestPath);
return Mono.just(exchange.getRequest());
} else {
logger.info("POST Request replaying from cache: {}", requestPath);
return Mono.just(request);
}
} else {
return Mono.empty();
}
});
}
}
It would be nice if something like this existed out of the box in spring security.
Possible Concerns
- There may not be many use cases other than mine, but I did see other discussions here: https://stackoverflow.com/questions/21958224/how-to-enable-spring-security-post-redirect-after-log-in-with-csrf
- There may be security issue with saving post details, also note SAML posts need to bypass CSRF.
- It is hard to to serialize/save the formData in the reactive request. I end up actually converting the
POST
to aGET
to get it to work.