-
Notifications
You must be signed in to change notification settings - Fork 317
Description
basically, the Wss4jSecurityInterceptor delegates to a SpringSecurityPasswordValidationCallbackHandler which in turn uses Spring Security to look up a User and get its password. That passowrd will be (hopefully) encoded using a PasswordEncoder.
In regular Spring Security HTTP Basic authentication on any other web scenario, Spring Security takes the password from the User#getPassword, finds the prefix (eg: {bcrypt}), and uses it to find the PasswordEncoder that matches the algorithm and then uses that PasswordEncoder to encode the plaintext passwrd sent by the client and then uses the PasswordEncoder to compare the two encoded passwords (the one encoded from the client and the one from the User.).
What happens in Spring WS is different. It compares the encoded password with the plaintext password in the SOAP message. I suppose we could try to encode the password in the SOAP message, but the comparison would still fail because, for example, BCrypt encodings have randomness and seeds so even given the same input, you might get different outputs. This is why we'd need the PasswordEncoder#matches, not just the call to MessageDigest.isEqual in UsernameTokenValidator#verifyDigestPassword, which does a straight byte for byte equality check and doesn't know about Spring Security's encodings.
AFAICT, the only way to make this work is to store passwords with the PasswordEncoder using plaintext, but this is insecure and - for most systems - not an option because the passwords will already be encoded.
i mention all this thinking that i am missing something, somehow.
i have a Spring WS app using Spring Boot 4 and the spring-boot-starter-web-services starter and org.springframework.ws : spring-ws-security
here's the relevant Spring Security + Spring WS integration configuration:
@Configuration
class SecurityConfiguration {
@Configuration
static class SecurityWsConfigurer implements WsConfigurer {
private final Wss4jSecurityInterceptor securityInterceptor;
SecurityWsConfigurer(Wss4jSecurityInterceptor securityInterceptor) {
this.securityInterceptor = securityInterceptor;
}
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(this.securityInterceptor);
}
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) {
var users = Set.of("stephane", "rob", "josh")
.stream()
.map(username -> User //
.withUsername(username)//
.password(passwordEncoder.encode("pw")) //
.roles("USER") //
.build() //
)
.toList();
return new InMemoryUserDetailsManager(users);
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(a -> a
.requestMatchers("/ws/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
SpringSecurityPasswordValidationCallbackHandler springSecurityPasswordValidationCallbackHandler(UserDetailsService service) {
var security = new SpringSecurityPasswordValidationCallbackHandler();
security.setUserDetailsService(service);
return security;
}
@Bean
Wss4jSecurityInterceptor wss4jSecurityInterceptor(
SpringSecurityPasswordValidationCallbackHandler handler) {
var ws4jsi = new Wss4jSecurityInterceptor();
ws4jsi.setValidationActions("UsernameToken");
ws4jsi.setValidationCallbackHandler(handler);
return ws4jsi;
}
}Here's the request I'm sending to the endpoint
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:gs="http://example.com/ws">
<soapenv:Header>
<wsse:Security soapenv:mustUnderstand="1">
<wsse:UsernameToken>
<wsse:Username>josh</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">pw</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<gs:getCountryRequest>
<gs:name>Spain</gs:name>
</gs:getCountryRequest>
</soapenv:Body>
</soapenv:Envelope>
this way
#!/usr/bin/env bash
curl -v --header "content-type: text/xml" -d @request-secure.xml http://localhost:8080/ws