Expected Behavior

In Spring boot 2.7.18 existing code was working fine for SAML Login but after migrating it to Spring boot 3.5.0 SAML login is giving error on Authenticating SAML Response

Code --

public class Security {

@Autowired
private ResourceLoader resourceLoader;

public InputStream getXmlFileAsStream() throws IOException {
    Resource resource = resourceLoader.getResource("classpath:metadata.xml");
    return resource.getInputStream();
}   

@Autowired
private SamlUserAuth samlUserDetailService;

@Bean
public SamlUserAuth samlUserDetailService() {
return new SamlUserAuth();
}

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() throws Exception {
RelyingPartyRegistration registration = RelyingPartyRegistrations.fromMetadata(getXmlFileAsStream())
.registrationId("saml").entityId("https://www.application.com").assertionConsumerServiceLocation("https://www.application.com/saml")
.singleLogoutServiceLocation("https://www.application.com/logout/saml").singleLogoutServiceResponseLocation("https://www.application.com/logout/saml")
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated());
    http.saml2Login(saml2 -> {
        try {
            saml2.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository())
            .loginProcessingUrl("/saml")
            .authenticationManager(new Saml2UserDetailsAuthenticationManager(samlUserDetailService))
            .defaultSuccessUrl("/home").loginPage("/customLogin")
            .failureHandler(new CustomSamlAuthenticationFailureHandler()).permitAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).saml2Logout(Customizer.withDefaults());
    }
}


public class Saml2UserDetailsAuthenticationManager implements AuthenticationManager {
private SamlUserAuth userDetailsServiceImp;
private OpenSaml4AuthenticationProvider openSamlAuthProvider = new OpenSaml4AuthenticationProvider();
public Saml2UserDetailsAuthenticationManager(SamlUserAuth userDetailsServiceImp) {
    this.userDetailsServiceImp = userDetailsServiceImp;
}

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Saml2Authentication saml2AuthenticationResult = (Saml2Authentication) openSamlAuthProvider.authenticate(authentication);
    Saml2AuthenticatedPrincipal  principal = (Saml2AuthenticatedPrincipal) saml2AuthenticationResult.getPrincipal();
    saml2AuthenticationResult.getSaml2Response();
    UserDetails userDetails = userDetailsServiceImp.loadUserByUsername(principal.getName());
    return new Saml2WithUserDetailsAuthentication(saml2AuthenticationResult, userDetails);
    }
}



public class SamlUserAuth implements UserDetailsService {
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // usual config
    }
}



public class Saml2WithUserDetailsAuthentication implements Authentication {
private UserDetails userDetails = null;
private Saml2Authentication saml2Authentication = null;

public Saml2WithUserDetailsAuthentication(Saml2Authentication saml2Authentication, UserDetails userDetails) {
        this.saml2Authentication = saml2Authentication;
        this.userDetails = userDetails;
    }
}

Current Behavior

Error logs

2025-07-28 08:03:54.663 [http-nio-7701-exec-2] WARN o.o.s.s.a.i.AbstractSubjectConfirmationValidator:274 - Valid InResponseTo was not available from the validation context, unable to evaluate SubjectConfirmationData@InResponseTo 2025-07-28 08:03:54.663 [http-nio-7701-exec-2] DEBUG o.o.s.s.a.SAML20AssertionValidator:896 - No subject confirmation methods were met for assertion with ID 'id-4ee40a00-cfe3-4fe0-b452-fa76810bde30' 2025-07-28 08:03:54.663 [http-nio-7701-exec-2] TRACE o.s.s.s.p.s.a.BaseOpenSamlAuthenticationProvider:370 - Found 2 validation errors in SAML response [id-0b21bdec-b0a4-41b0-85a5-80a9f044da09]: [[invalid_in_response_to] The response contained an InResponseTo attribute [ARQa7b1d4f-b780-4611-8588-e9589dccb8c8] but no saved authentication request was found, [invalid_assertion] Invalid assertion [id-4ee40a00-cfe3-4fe0-b452-fa76810bde30] for SAML response [id-0b21bdec-b0a4-41b0-85a5-80a9f044da09]: No subject confirmation methods were met for assertion with ID 'id-4ee40a00-cfe3-4fe0-b452-fa76810bde30'] 2025-07-28 08:03:54.664 [http-nio-7701-exec-2] TRACE o.s.s.s.p.s.w.a.Saml2WebSsoAuthenticationFilter:372 - Failed to process authentication request org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException: The response contained an InResponseTo attribute [ARQa7b1d4f-b780-4611-8588-e9589dccb8c8] but no saved authentication request was found

Can you please help me out ?

Comment From: re1709

Try setting the security context, i think this is now opt in with spring boot 3.

.saml2Login(saml2 -> saml2
                        .securityContextRepository(new HttpSessionSecurityContextRepository())

Comment From: singhishere003

Thank you for help @re1709 but still i am facing the same issue after using your suggestion.

Comment From: re1709

Thank you for help @re1709 but still i am facing the same issue after using your suggestion.

Ah sorry!

Definitely ran into this issue a while ago when upgrading from spring boot 2. There was several issues though, so i'm struggling to remember what fixed this one in particular. Our setup is also different from yours, as we have a custom username and password filter.

Will dig through some things tomorrow and see if i can work it out - unless someone who's more knowledgable can clarify!

Comment From: re1709

Checked our logs, and looks like it was a same site issue with the cookie serializer breaking the validation context. Registering this bean fixed the issue:

  @Bean
  public DefaultCookieSerializerCustomizer cookieSerializerCustomizer() {
        return cookieSerializer -> {
            // SAML breaks when using spring session as it sets LAX on cookie by default
            cookieSerializer.setSameSite(null);
        };
  }

I think we could set this to secure when running under https but not tested.

Comment From: singhishere003

@re1709 Yeah! It worked. But my Question is in Spring boot 2.7.18 it was working without changing the cookie configuration changes? This solution has some security concern?

Comment From: re1709

This is all coming back to me now. We have a production app, and development app. The fix with the cookie serializer we are using in the development app as it was more tricky, but you’re right this does have concerns.

In the production app, we explored this solution by switching the repository instead of customising the cookie: https://github.com/spring-projects/spring-security/issues/14793

We were hoping spring security would eventually address this, and looks like the repository is making its way into 6.5.X

Some detail about the cookie lax issue in spring security 6: https://github.com/spring-projects/spring-security/issues/14013

Comment From: singhishere003

@re1709 I don't know what happened but the now i am getting the same error if i logout and login in same browser. Yesterday it was working fine but now again the same issue.

Comment From: re1709

Sorry for the delay. I've been looking into this within our own codebase. The CacheRepository now in spring security 6.5 doesn't work with OpenSaml4 (i'm guessing it was designed for the deprecated saml classes). Fortunately it just needs a one line tweak. This class is working for me as an alternative for customising the cookie same site attribute:

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

public final class CustomCacheSaml2AuthenticationRequestRepository
        implements Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {

    private Cache cache = new ConcurrentMapCache("authentication-requests");

    @Override
    public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
        String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
        Assert.notNull(relayState, "relayState must not be null");
        return this.cache.get(relayState, AbstractSaml2AuthenticationRequest.class);
    }

    @Override
    public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
            HttpServletRequest request, HttpServletResponse response) {
        String relayState = authenticationRequest.getRelayState();
        Assert.notNull(relayState, "relayState must not be null");
        this.cache.put(relayState, authenticationRequest);
    }

    @Override
    public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(HttpServletRequest request,
            HttpServletResponse response) {
        String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
        Assert.notNull(relayState, "relayState must not be null");
        AbstractSaml2AuthenticationRequest authenticationRequest = this.cache.get(relayState,
                AbstractSaml2AuthenticationRequest.class);
        if (authenticationRequest == null) {
            return null;
        }
        this.cache.evict(relayState);
        return authenticationRequest;
    }

    /**
     * Use this {@link Cache} instance. The default is an in-memory cache, which means it
     * won't work in a clustered environment. Instead, replace it here with a distributed
     * cache.
     * @param cache the {@link Cache} instance to use
     */
    public void setCache(Cache cache) {
        this.cache = cache;
    }

}

Then using the class in configuration:

@Bean
public Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
        return new CustomCacheSaml2AuthenticationRequestRepository();
}

This however doesn't support scaled environments, such as kubernetes so i'm going to try and implement a database version of this repository.

Regarding the cookie customiser solution, i think it's valid to set it to SameSite.NONE if you're running under https since single sign on is involved:

@Bean
public DefaultCookieSerializerCustomizer cookieSerializerCustomizer() {
        return cookieSerializer -> {
            // SAML breaks when using spring session as its sets LAX on cookie by default
            cookieSerializer.setSameSite(Cookie.SameSite.NONE.attributeValue());
        };
}

Then for local development can set it to null through a specific profile being active.

Comment From: singhishere003

Can anyone from spring security team tell me how do it without setting sameSite to None?

Comment From: singhishere003

@rwinch can you please suggest what should i do in this case?

Comment From: rwinch

cc @jzheaux

Comment From: singhishere003

@jzheaux Could you please help me in above issue?