Search code examples
javaspringspring-securitysamlspring-saml

Pass URL Parameters to Spring Security custom LogoutSuccessHandler


The server i am deploying is this https://github.com/OpenConext/OpenConext-oidc , i am extending it's logout capabilities , (Logout etc).

Now i have a request , the logout is done using : http:/www.example.com:8080/server app/saml/logout ,

i want to add parameters on the URL like this . http:/www.example.com:8080/server app/saml/logout?value=www.youtube.com , so i can redirect to different pages the user .


For that purpose i have created a custom CustomLogoutSuccessHandler :

public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        if (authentication != null && authentication.getDetails() != null) {
            try {
                request.getSession().invalidate();
                System.out.println("User Successfully Logout");
                //you can add more codes here when the user successfully logs out,
                //such as updating the database for last active.
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //Set the Server Status
        httpServletResponse.setStatus(HttpServletResponse.SC_OK);

        //redirect to login
        String queryString = request.getParameter("value");

        //Check if no parameters have been passed
        if (queryString == null) {
            httpServletResponse.sendRedirect("http://www.youtube.com");
        } else {
            httpServletResponse.sendRedirect("http://www." + queryString + ".com");
        }
    }

}

The problem is that the request.getParameter("value"); always returns null! Why that happens ? I am catching too late the URL or what?

I am always getting back the url `http:/www.example.com:8080/server app/saml/SingleLogout


On the user-context.xml (Logout is defined like this)(it works here there is no problem).

...

<!-- Filters for processing of SAML messages -->
    <bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
        <security:filter-chain-map request-matcher="ant">
            <security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
            <security:filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter"/>
            <security:filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter"/>
            <security:filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter"/>
            <security:filter-chain pattern="/saml/SSOHoK/**" filters="samlWebSSOHoKProcessingFilter"/>
            <security:filter-chain pattern="/saml/SingleLogout/**" filters="samlLogoutProcessingFilter"/>
        </security:filter-chain-map>
    </bean>

!-- Handler for successful logout -->
    <bean id="successLogoutHandler" class="oidc.security.CustomLogoutSuccessHandler"></bean>

...

Solution

  • Solution

    1) Created a custom SAMLLogoutFilter (CustomSAMLLogoutFilter) and i am passing the url of the original request to the CustomLogoutHandler:

    /*
     * To change this license header, choose License Headers in Project Properties.
     * To change this template file, choose Tools | Templates
     * and open the template in the editor.
     */
    package oidc.security;
    
    import java.io.IOException;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.springframework.security.saml.SAMLLogoutFilter;
    import org.springframework.security.web.authentication.logout.LogoutHandler;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    
    /**
     *
     * @author GOXR3PLUS
     */
    public class CustomSAMLLogoutFilter extends SAMLLogoutFilter {
    
        private LogoutSuccessHandler logoutSuccessHandler;
    
        public CustomSAMLLogoutFilter(String successUrl, LogoutHandler[] localHandler, LogoutHandler[] globalHandlers) {
            super(successUrl, localHandler, globalHandlers);
        }
    
        public CustomSAMLLogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler[] localHandler, LogoutHandler[] globalHandlers) {
            super(logoutSuccessHandler, localHandler, globalHandlers);
            this.logoutSuccessHandler = logoutSuccessHandler;
        }
    
        @Override
        public void processLogout(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            super.processLogout(request, response, chain);
    
            //   System.out.println("Hello from [ CustomSAMLLogoutFilter ] ");
            //Lets print some information here
            System.out.println("FULL HttpServletRequest URL is : " + getFullURL(request));
    
            //Downcast and pass it as parameter
            ((CustomLogoutSuccessHandler) logoutSuccessHandler).setOriginalURL(getFullURL(request));
            //   System.out.println("Chao chao from [ CustomSAMLLogoutFilter ] \n");
        }
    
        /**
         * Returns the full URL of the HTTPServletRequest
         */
        public static String getFullURL(HttpServletRequest request) {
            StringBuffer requestURL = request.getRequestURL();
            String queryString = request.getQueryString();
    
            if (queryString == null) {
                return requestURL.toString();
            } else {
                return requestURL.append('?').append(queryString).toString();
            }
        }
    
    }
    

    2) The code of the CustomLogoutHandler :

    /*
     * To change this license header, choose License Headers in Project Properties.
     * To change this template file, choose Tools | Templates
     * and open the template in the editor.
     */
    package oidc.security;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    //import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.net.URL;
    import java.util.List;
    import java.util.regex.Pattern;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.util.UriComponentsBuilder;
    
    public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    
        /**
         * This variable is used in order to keep track of the original URL the user
         * passed before he logged out
         */
        private String originalURL;
    
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            if (authentication != null && authentication.getDetails() != null) {
                try {
                    request.getSession().invalidate();
                    System.out.println("User Successfully Logout");
                    //you can add more codes here when the user successfully logs out,
                    //such as updating the database for last active.
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            //Set the Server Status
            response.setStatus(HttpServletResponse.SC_OK);
    
            //----------- Choose where to redirect------------------
            System.out.println("Hello from [ CustomLogoutSuccessHandler ] \n");
            System.out.println("Original URL is : " + originalURL);
    
            //Check if any parameters have been passed
            String redirectURL = null;
            if (originalURL != null) {
    
                try {
                    //Get all the parameters from the url
                    MultiValueMap<String, String> parameters
                            = UriComponentsBuilder.fromUriString(originalURL).build().getQueryParams();
    
                    //--Get the parameter value 
                    List<String> value = parameters.get("redirect");
                    if (value.size() != 0) { //if it exists
                        redirectURL = value.get(0);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
    
            // Decide where to redirect
            System.out.println(" Redirect : " + redirectURL);
            if (redirectURL == null) {
                response.sendRedirect("/oidc/");
            } else {
                response.sendRedirect(redirectURL);
            }
        }
    
        public void setOriginalURL(String originalURL) {
            this.originalURL = originalURL;
        }
    
    }
    

    3) On the user-context.xml i changed only two things

    <!-- Handler for successful logout --> <bean id="successLogoutHandler" class="oidc.security.CustomLogoutSuccessHandler"></bean>

    and

    <!-- Override default logout processing filter with the one processing SAML messages --> <bean id="samlLogoutFilter" class="oidc.security.CustomSAMLLogoutFilter"> <constructor-arg index="0" ref="successLogoutHandler"/> <constructor-arg index="1" ref="logoutHandler"/> <constructor-arg index="2" ref="logoutHandler"/> </bean>

    in order for them to see the new custom java files java/..../security


    Finally case you want to see all the user-context.xml here it is :

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
      Copyright 2015 The MITRE Corporation 
        and the MIT Kerberos and Internet Trust Consortium
    
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
      limitations under the License.
    -->
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:security="http://www.springframework.org/schema/security"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd"
    
           profile="!local">
    
    
        <context:property-placeholder location="classpath:application.oidc.properties"/>
    
        <!-- Enable auto-wiring -->
        <context:annotation-config/>
    
        <!-- Scan for auto-wiring classes in spring saml packages -->
        <context:component-scan base-package="org.springframework.security.saml"/>
    
        <!-- Unsecured pages -->
        <security:http security="none" pattern="/translate-sp-entity-id" create-session="never"/>
    
        <!-- Secured pages with SAML as entry point -->
        <security:http entry-point-ref="samlEntryPoint" use-expressions="true" create-session="never">
            <security:intercept-url pattern="/authorize" access="hasRole('ROLE_USER')" />
            <security:intercept-url pattern="/**" access="permitAll" />
            <security:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
            <security:custom-filter before="PRE_AUTH_FILTER" ref="clientIdFilter"/>
            <security:custom-filter ref="authRequestFilter" after="SECURITY_CONTEXT_FILTER" />
            <security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
            <!--<security:anonymous />-->
            <security:headers>
                <security:frame-options policy="DENY" />
            </security:headers>
            <security:csrf request-matcher-ref="apiCsrfProtectionMatcher"/>
        </security:http>
    
        <!-- Logger for SAML messages and events -->
        <bean id="clientIdFilter" class="oidc.security.ClientIdFilter"/>
    
        <!-- Filters for processing of SAML messages -->
        <bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
            <security:filter-chain-map request-matcher="ant">
                <security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
                <security:filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter"/>
                <security:filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter"/>
                <security:filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter"/>
                <security:filter-chain pattern="/saml/SSOHoK/**" filters="samlWebSSOHoKProcessingFilter"/>
                <security:filter-chain pattern="/saml/SingleLogout/**" filters="samlLogoutProcessingFilter"/>
            </security:filter-chain-map>
        </bean>
    
    
    
        <!-- Handler deciding where to redirect user after successful login -->
        <bean id="successRedirectHandler"
              class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
            <property name="defaultTargetUrl" value="/"/>
        </bean>
    
        <!--
        Use the following for interpreting RelayState coming from unsolicited response as redirect URL:
        <bean id="successRedirectHandler" class="org.springframework.security.saml.SAMLRelayStateSuccessHandler">
           <property name="defaultTargetUrl" value="/" />
        </bean>
        -->
    
        <!-- Handler deciding where to redirect user after failed login -->
        <bean id="failureRedirectHandler"
              class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
            <property name="useForward" value="true"/>
            <property name="defaultFailureUrl" value="/error.jsp"/>
        </bean>
    
        <!-- Handler for successful logout -->
        <bean id="successLogoutHandler" class="oidc.security.CustomLogoutSuccessHandler"></bean>
    
        <!-- Override default logout processing filter with the one processing SAML messages -->
        <bean id="samlLogoutFilter" class="oidc.security.CustomSAMLLogoutFilter">
            <constructor-arg index="0" ref="successLogoutHandler"/>
            <constructor-arg index="1" ref="logoutHandler"/>
            <constructor-arg index="2" ref="logoutHandler"/>
        </bean>
    
        <!-- Filter processing incoming logout messages -->
        <!-- First argument determines URL user will be redirected to after successful global logout -->
        <bean id="samlLogoutProcessingFilter" class="org.springframework.security.saml.SAMLLogoutProcessingFilter">
            <constructor-arg index="0" ref="successLogoutHandler"/>
            <constructor-arg index="1" ref="logoutHandler"/>
        </bean>
    
    
        <security:authentication-manager alias="authenticationManager">
            <!-- Register authentication manager for SAML provider -->
            <security:authentication-provider ref="samlAuthenticationProvider"/>
        </security:authentication-manager>
    
        <!-- Logger for SAML messages and events -->
        <bean id="samlLogger" class="org.springframework.security.saml.log.SAMLDefaultLogger"/>
    
        <bean id="keyStoreLocator" class="oidc.saml.KeyStoreLocator"/>
    
        <!-- Central storage of cryptographic keys -->
        <bean id="keyManager" class="org.springframework.security.saml.key.JKSKeyManager">
            <constructor-arg>
                <bean factory-bean="keyStoreLocator"
                      factory-method="createKeyStore">
                    <constructor-arg value="${idp.entity.id}"/>
                    <constructor-arg value="${idp.public.certificate}"/>
                    <constructor-arg value="${sp.entity.id}"/>
                    <constructor-arg value="${sp.public.certificate}"/>
                    <constructor-arg value="${sp.private.key}"/>
                    <constructor-arg value="${sp.passphrase}"/>
                </bean>
            </constructor-arg>
            <constructor-arg>
                <map>
                    <entry key="${sp.entity.id}" value="${sp.passphrase}"/>
                </map>
            </constructor-arg>
            <constructor-arg type="java.lang.String" value="${sp.entity.id}"/>
        </bean>
    
        <!-- Entry point to initialize authentication, default values taken from properties file -->
        <bean id="samlEntryPoint" class="oidc.saml.ProxySAMLEntryPoint"/>
    
        <!-- Filter automatically generates default SP metadata -->
        <bean id="metadataGeneratorFilter" class="org.springframework.security.saml.metadata.MetadataGeneratorFilter">
            <constructor-arg>
                <bean class="org.springframework.security.saml.metadata.MetadataGenerator">
                    <property name="entityId" value="${sp.entity.id}"/>
                    <property name="entityBaseURL" value="${sp.entity.base.url}"/>
                    <property name="extendedMetadata">
                        <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
                            <property name="signMetadata" value="true"/>
                            <property name="idpDiscoveryEnabled" value="false"/>
                        </bean>
                    </property>
                </bean>
            </constructor-arg>
        </bean>
    
        <!-- The filter is waiting for connections on URL suffixed with filterSuffix and presents SP metadata there -->
        <bean id="metadataDisplayFilter" class="org.springframework.security.saml.metadata.MetadataDisplayFilter"/>
    
        <bean id="metadataManager" class="org.springframework.security.saml.metadata.CachingMetadataManager">
            <constructor-arg>
                <list>
                    <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
                        <constructor-arg>
                            <bean class="org.opensaml.saml2.metadata.provider.HTTPMetadataProvider">
                                <constructor-arg>
                                    <value type="java.lang.String">${idp.metadata.url}</value>
                                </constructor-arg>
                                <constructor-arg>
                                    <value type="int">30000</value>
                                </constructor-arg>
                                <property name="parserPool" ref="parserPool"/>
                                <property name="requireValidMetadata" value="false"/>
                            </bean>
                        </constructor-arg>
                    </bean>
                </list>
            </constructor-arg>
        </bean>
    
        <!-- IDP Metadata configuration - paths to metadata of IDPs in circle of trust is here -->
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
            <constructor-arg ref="metadataManager"/>
        </bean>
    
        <bean id="defaultSAMLUserDetailsService" class="oidc.saml.DefaultSAMLUserDetailsService">
            <constructor-arg value="${sp.entity.id}"/>
        </bean>
    
        <!-- SAML Authentication Provider responsible for validating of received SAML messages -->
        <bean id="samlAuthenticationProvider" class="oidc.saml.FederatedSAMLAuthenticationProvider">
            <property name="userDetails" ref="defaultSAMLUserDetailsService"/>
        </bean>
    
        <!-- Provider of default SAML Context -->
        <bean id="contextProvider" class="org.springframework.security.saml.context.SAMLContextProviderImpl"/>
    
        <!-- Processing filter for WebSSO profile messages -->
        <bean id="samlWebSSOProcessingFilter" class="org.springframework.security.saml.SAMLProcessingFilter">
            <property name="authenticationManager" ref="authenticationManager"/>
            <property name="authenticationSuccessHandler" ref="successRedirectHandler"/>
            <property name="authenticationFailureHandler" ref="failureRedirectHandler"/>
        </bean>
    
        <!-- Processing filter for WebSSO Holder-of-Key profile -->
        <bean id="samlWebSSOHoKProcessingFilter" class="org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter">
            <property name="authenticationManager" ref="authenticationManager"/>
            <property name="authenticationSuccessHandler" ref="successRedirectHandler"/>
            <property name="authenticationFailureHandler" ref="failureRedirectHandler"/>
        </bean>
    
        <!-- Logout handler terminating local session -->
        <bean id="logoutHandler"
              class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler">
            <property name="invalidateHttpSession" value="false"/>
        </bean>
    
        <!-- Class loading incoming SAML messages from httpRequest stream -->
        <bean id="processor" class="org.springframework.security.saml.processor.SAMLProcessorImpl">
            <constructor-arg>
                <list>
                    <ref bean="redirectBinding"/>
                    <ref bean="postBinding"/>
                    <ref bean="artifactBinding"/>
                    <ref bean="soapBinding"/>
                    <ref bean="paosBinding"/>
                </list>
            </constructor-arg>
        </bean>
    
        <!-- SAML 2.0 WebSSO Assertion Consumer -->
        <bean id="webSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl">
            <property name="maxAuthenticationAge" value="43200"/>
        </bean>
    
        <!-- SAML 2.0 Holder-of-Key WebSSO Assertion Consumer -->
        <bean id="hokWebSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl"/>
    
        <!-- SAML 2.0 Web SSO profile -->
        <bean id="webSSOprofile" class="org.springframework.security.saml.websso.WebSSOProfileImpl"/>
    
        <!-- SAML 2.0 Holder-of-Key Web SSO profile -->
        <bean id="hokWebSSOProfile" class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl"/>
    
        <!-- SAML 2.0 ECP profile -->
        <bean id="ecpprofile" class="org.springframework.security.saml.websso.WebSSOProfileECPImpl"/>
    
        <!-- SAML 2.0 Logout Profile -->
        <bean id="logoutprofile" class="org.springframework.security.saml.websso.SingleLogoutProfileImpl"/>
    
        <!-- Bindings, encoders and decoders used for creating and parsing messages -->
        <bean id="postBinding" class="org.springframework.security.saml.processor.HTTPPostBinding">
            <constructor-arg ref="parserPool"/>
            <constructor-arg ref="velocityEngine"/>
        </bean>
    
        <bean id="redirectBinding" class="org.springframework.security.saml.processor.HTTPRedirectDeflateBinding">
            <constructor-arg ref="parserPool"/>
        </bean>
    
        <bean id="artifactBinding" class="org.springframework.security.saml.processor.HTTPArtifactBinding">
            <constructor-arg ref="parserPool"/>
            <constructor-arg ref="velocityEngine"/>
            <constructor-arg>
                <bean class="org.springframework.security.saml.websso.ArtifactResolutionProfileImpl">
                    <constructor-arg>
                        <bean class="org.apache.commons.httpclient.HttpClient">
                            <constructor-arg>
                                <bean class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager"/>
                            </constructor-arg>
                        </bean>
                    </constructor-arg>
                    <property name="processor">
                        <bean class="org.springframework.security.saml.processor.SAMLProcessorImpl">
                            <constructor-arg ref="soapBinding"/>
                        </bean>
                    </property>
                </bean>
            </constructor-arg>
        </bean>
    
        <bean id="soapBinding" class="org.springframework.security.saml.processor.HTTPSOAP11Binding">
            <constructor-arg ref="parserPool"/>
        </bean>
    
        <bean id="paosBinding" class="org.springframework.security.saml.processor.HTTPPAOS11Binding">
            <constructor-arg ref="parserPool"/>
        </bean>
    
        <!-- Initialization of OpenSAML library-->
        <bean class="org.springframework.security.saml.SAMLBootstrap"/>
    
        <!-- Initialization of the velocity engine -->
        <bean id="velocityEngine" class="org.springframework.security.saml.util.VelocityFactory" factory-method="getEngine"/>
    
        <!-- XML parser pool needed for OpenSAML parsing -->
        <bean id="parserPool" class="org.opensaml.xml.parse.StaticBasicParserPool" init-method="initialize">
            <property name="builderFeatures">
                <map>
                    <entry key="http://apache.org/xml/features/dom/defer-node-expansion" value="false"/>
                </map>
            </property>
        </bean>
    
        <bean id="parserPoolHolder" class="org.springframework.security.saml.parser.ParserPoolHolder"/>
    </beans>