spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.85k stars 5.91k forks source link

login url for Saml 2.0 is not working after migration to spring boot 3.3.4 from 2.7.18 #15979

Closed shubhamwankh closed 4 weeks ago

shubhamwankh commented 1 month ago

Describe the bug I've migrated from spring boot 2.7.18 to spring boot 3.3.4, I've a SAML 2.0 with OpenSaml in my project, previously I used to redirect to the SAML login page using /login/saml2/sso/okta but after migrating the URL is not working and is directly returning 404, I've checked and found that request is directly going to Saml2WebSsoAuthenticationRequestFilter and then to the OpenSamlAuthenticationRequestResolver.resolve(-,-) and using request matcher for \saml2\authenticate\{registrationId} and since the login url does not match the authentication is set as null

it's giving below page, no relying party defination found is because of favicon.icon call igonore it I've checked and relying party configs are already present

image

Here is stack trace

Saml2AuthenticationException{error=[relying_party_registration_not_found] No relying party registration found}
    at org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter.attemptAuthentication(Saml2WebSsoAuthenticationFilter.java:127)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:231)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter.doFilterInternal(Saml2WebSsoAuthenticationRequestFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
    at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
    at java.base/java.lang.Thread.run(Thread.java:1583)

To Reproduce here is the saml github POC project, https://github.com/shubhamwankh/SamlPoc

to reproduce make 'http://localhost:8080/login/saml2/sso/okta'

Expected behavior after hitting above url it should redirect to okta login page

Sample

A link to a GitHub repository with a minimal, reproducible sample.

Reports that include a sample will take priority over reports that do not. At times, we may require a sample, so it is good to try and include a sample up front.

jzheaux commented 1 month ago

Thanks for the report, @shubhamwankh. The reproducer is helpful, and it also generates a question.

To navigate to /login/saml2/sso/okta without a SAMLResponse is an error. Perhaps the error message needs improvement; however, can you elaborate on when you need to navigate to that URL in the way you described?

shubhamwankh commented 1 month ago

@jzheaux In our project we have multiple login options and SAML is one of them, we have configured above URL (/login/saml2/sso/okta) for SAML sso login if the user is not logged in already, for that we cannot change it as of now.

since the same url was working previously because the access was denied for the URL (/login/saml2/sso/okta) if the user was not authenticated and the user was redirected to the login page through ExceptionTranslationFilter and we are expecting to continue with it the same url.

jzheaux commented 1 month ago

I can see why you'd want things to stay working as they were for you, @shubhamwankh. Sometimes these things happen across a major release upgrade. Let me see what I can do to help here.


In reviewing the documentation, I found the following:

As configured earlier, the application processes any POST /login/saml2/sso/{registrationId} request containing a SAMLResponse parameter:


POST /login/saml2/sso/adfs HTTP/1.1

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...


While the above documentation is the most recent, the same is stated in [the 5.7 docs](https://docs.spring.io/spring-security/reference/5.7/servlet/saml2/login/overview.html#_runtime_expectations).

In other words, `/login/saml2/sso/okta` is the endpoint that Okta should POST to. Other uses of this endpoint aren't supported.

To induce login, the endpoint is [`/saml2/authenticate/okta`](https://docs.spring.io/spring-security/reference/servlet/saml2/login/authentication-requests.html).

The docs also contain a summary of all [the default SAML 2.0 URIs](https://docs.spring.io/spring-security/reference/5.7/servlet/saml2/login/overview.html#servlet-saml2login-rpr-uripatterns).

----

Are you able to correct your application to redirect unauthenticated users to `/saml2/authenticate/okta`? Note that when I do this with your sample, it redirects correctly to Okta.

If not, you can create a post-processor for the filter like so:

```java
@Component
public class Saml2WebSsoAuthenticationFilterPostProcessor 
        implements ObjectPostProcessor<Saml2WebSsoAuthenticationFilter> {

    private final RelyingPartyRegistrationRepository registrations;

    public Saml2WebSsoAuthenticationFilterPostProcessor(RelyingPartyRegistrationRepository registrations) {
        this.registrations = registrations;
    }

    @Override
    public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter object) {
        return new Saml2WebSsoAuthenticationFilter(this.registrations) {
            @Override
            protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
                // bypass filter if SAMLResponse not present
                return super.requiresAuthentication(request, response) &&
                        request.getParameter(Saml2ParameterNames.SAML_RESPONSE) != null;
            }
        };
    }
}

And then provide that post-processor to your saml2Login configuration:

@Bean
- SecurityFilterChain configure(HttpSecurity http) throws Exception { 
+ SecurityFilterChain configure(HttpSecurity http, Saml2WebSsoAuthenticationFilterPostProcessor p) throws Exception {
    // ...

        .saml2Login(saml2 -> saml2
            .authenticationManager(new ProviderManager(authenticationProvider))
+            .addObjectPostProcessor(p)
        )

    // ...
}

I've submitted a PR to your repo so that you can review this suggestion, though I hope that you will be able to update your application to point to /saml2/authenticate/okta instead.


I think that the error handling in Saml2WebSsoAuthenticationFilter can also be improved. Other Spring Security endpoints allow the request to pass through the rest of the filter chain when the needed request parameters (like SAMLResponse) aren't present. I've opened https://github.com/spring-projects/spring-security/issues/16000 to address this.

shubhamwankh commented 4 weeks ago

Thank you very much @jzheaux for the solution.