Skip to content

its not clear how to use Spring Security with Wss4jSecurityInterceptor for UsernameToken authentication #1745

@joshlong

Description

@joshlong

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions