0

I am seeking solutions to migrate current holistic system into microservices architecture. I want to use Spring Integration and Spring Security to integrate and secure the services. According to my understanding, to secure backend services is more like Single Sign On (SSO). I use Jasig CAS 4.2.7 (seems working fine with Spring Security) to authenticate users centrally, Spring Integration 4.2.11.RELEASE and Spring Security 4.0.4.RELEASE.

I have created a Maven project with two modules named web and service which are both web application. I deploy the three war files on same local Tomcat (version 7.0.36) and just add jimi and bob into CAS properties file to ensure them passing the authentication of CAS. When I try to access URL http://localhost:8080/prototype-integration-security-web/user, I got authenticated into front-end application but access forbidden on backend services.

The POM file looks as below.

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>prototype.integration.security</groupId>
  <artifactId>prototype-integration-security</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>pom</packaging>

  <name>prototype-integration-security</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.5.1</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.6</version>
        <configuration>
          <warName>${project.name}</warName>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-http</artifactId>
        <version>4.2.11.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>4.0.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.2.7.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-jcl</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>7.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-security</artifactId>
        <version>4.2.11.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>4.0.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-cas</artifactId>
        <version>4.0.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.6.4</version>
    </dependency>
  </dependencies>
  <modules>
    <module>prototype-integration-security-web</module>
    <module>prototype-integration-security-service</module>
  </modules>
</project>

The deployment description files web.xml of two modules look same except the display name as following.

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                             http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="IntegrationSecurityWeb" version="3.0">
  <display-name>Integration Security Web Prototype</display-name>

  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

In the Spring application context configuration file of web module, dispatcher-servlet.xml looks as below.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:int="http://www.springframework.org/schema/integration"
       xmlns:int-http="http://www.springframework.org/schema/integration/http"
       xmlns:int-security="http://www.springframework.org/schema/integration/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/task
                           http://www.springframework.org/schema/task/spring-task.xsd
                           http://www.springframework.org/schema/security
                           http://www.springframework.org/schema/security/spring-security.xsd
                           http://www.springframework.org/schema/integration
                           http://www.springframework.org/schema/integration/spring-integration-4.2.xsd
                           http://www.springframework.org/schema/integration/http
                           http://www.springframework.org/schema/integration/http/spring-integration-http-4.2.xsd
                           http://www.springframework.org/schema/integration/security
                           http://www.springframework.org/schema/integration/security/spring-integration-security-4.2.xsd">

  <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg>
      <bean class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
        <constructor-arg>
          <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
            <property name="targetClass" value="org.apache.http.impl.client.HttpClients"/>
            <property name="targetMethod" value="createMinimal"/>
          </bean>
        </constructor-arg>
      </bean>
    </constructor-arg>
    <property name="messageConverters">
      <list>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.FormHttpMessageConverter">
        </bean>
      </list>
    </property>
  </bean>

  <bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
    <property name="service" value="http://localhost:8080/prototype-integration-security-web/login/cas" />
    <property name="sendRenew" value="false" />
  </bean>

  <!-- Access voters -->
  <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
    <constructor-arg name="decisionVoters">
      <list>
        <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
          <constructor-arg>
            <bean class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
              <property name="hierarchy">
                <value>
                  ROLE_ADMIN > ROLE_USER
                </value>
              </property>
            </bean>
          </constructor-arg>
        </bean>
        <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
      </list>
    </constructor-arg>
  </bean>

  <bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
    <property name="loginUrl" value="https://localhost:8443/cas/login" />
    <property name="serviceProperties" ref="serviceProperties" />
  </bean>

  <bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
  </bean>

  <!-- This filter handles a Single Logout Request from the CAS Server -->
  <bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />

  <!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
  <bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
    <constructor-arg value="http://localhost:8080/cas/logout" />
    <constructor-arg>
      <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
    </constructor-arg>
    <property name="filterProcessesUrl" value="/logout/cas" />
  </bean>

  <security:http entry-point-ref="casEntryPoint" access-decision-manager-ref="accessDecisionManager" use-expressions="false">
    <security:intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
    <security:intercept-url pattern="/**" access="ROLE_USER" />
    <security:form-login />
    <security:logout />
    <security:custom-filter before="LOGOUT_FILTER" ref="requestSingleLogoutFilter"/>
    <security:custom-filter before="CAS_FILTER" ref="singleLogoutFilter"/>
    <security:custom-filter position="CAS_FILTER" ref="casFilter" />
  </security:http>

  <security:user-service id="userService">
    <security:user name="jimi" password="jimi" authorities="ROLE_ADMIN" />
    <security:user name="bob" password="bob" authorities="ROLE_USER" />
  </security:user-service>

  <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
    <property name="authenticationUserDetailsService">
      <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <constructor-arg index="0" ref="userService" />
      </bean>
    </property>
    <property name="serviceProperties" ref="serviceProperties" />
    <property name="ticketValidator">
      <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
        <constructor-arg index="0" value="https://localhost:8443/cas" />
      </bean>
    </property>
    <property name="key" value="localCAS" />
  </bean>

  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider ref="casAuthenticationProvider" />
  </security:authentication-manager>

  <int:channel-interceptor order="99">
    <bean class="org.springframework.integration.security.channel.SecurityContextPropagationChannelInterceptor"/>
  </int:channel-interceptor>

  <task:executor id="pool" pool-size="5"/>

  <int:poller id="poller" default="true" fixed-rate="1000"/>

  <int-security:secured-channels>
    <int-security:access-policy pattern="user*" send-access="ROLE_USER" />
    <int-security:access-policy pattern="admin*" send-access="ROLE_ADMIN" />
  </int-security:secured-channels>

  <int-http:inbound-channel-adapter path="/user*" supported-methods="GET, POST" channel="userRequestChannel" />

  <int:channel id="userRequestChannel">
    <int:queue/>
  </int:channel>

  <int-http:outbound-channel-adapter url="http://localhost:8080/prototype-integration-security-service/query?ticket={ticket}"
                                     http-method="GET"
                                     rest-template="restTemplate"
                                     channel="userRequestChannel">
    <int-http:uri-variable name="ticket" expression="T(org.springframework.security.core.context.SecurityContextHolder).context.authentication.credentials"/>
  </int-http:outbound-channel-adapter>

  <int-http:inbound-channel-adapter path="/admin/callback*"
                                    supported-methods="GET, POST"
                                    channel="adminRequestChannel" />

  <int:channel id="adminRequestChannel">
    <int:queue/>
  </int:channel>

  <int:logging-channel-adapter id="logging" channel="adminRequestChannel" level="DEBUG" />
</beans>

In the context configuration file of service module, dispatcher-servlet.xml looks as following.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:int="http://www.springframework.org/schema/integration"
       xmlns:int-http="http://www.springframework.org/schema/integration/http"
       xmlns:int-security="http://www.springframework.org/schema/integration/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/task
                           http://www.springframework.org/schema/task/spring-task.xsd
                           http://www.springframework.org/schema/security
                           http://www.springframework.org/schema/security/spring-security.xsd
                           http://www.springframework.org/schema/integration
                           http://www.springframework.org/schema/integration/spring-integration-4.2.xsd
                           http://www.springframework.org/schema/integration/http
                           http://www.springframework.org/schema/integration/http/spring-integration-http-4.2.xsd
                           http://www.springframework.org/schema/integration/security
                           http://www.springframework.org/schema/integration/security/spring-integration-security-4.2.xsd">

  <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg>
      <bean class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
        <constructor-arg>
          <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
            <property name="targetClass" value="org.apache.http.impl.client.HttpClients"/>
            <property name="targetMethod" value="createMinimal"/>
          </bean>
        </constructor-arg>
      </bean>
    </constructor-arg>
    <property name="messageConverters">
      <list>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.FormHttpMessageConverter">
        </bean>
      </list>
    </property>
  </bean>

  <bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
    <property name="service" value="http://localhost:8080/prototype-integration-security-service/login/cas" />
    <property name="sendRenew" value="false" />
  </bean>

  <!-- Access voters -->
  <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
    <constructor-arg name="decisionVoters">
      <list>
        <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
          <constructor-arg>
            <bean class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
              <property name="hierarchy">
                <value>
                  ROLE_ADMIN > ROLE_USER
                </value>
              </property>
            </bean>
          </constructor-arg>
        </bean>
        <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
        <!-- <bean class="org.springframework.security.web.access.expression.WebExpressionVoter" /> -->
      </list>
    </constructor-arg>
  </bean>

  <bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
    <property name="loginUrl" value="https://localhost:8443/cas/login" />
    <property name="serviceProperties" ref="serviceProperties" />
  </bean>

  <bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
  </bean>

  <!-- This filter handles a Single Logout Request from the CAS Server -->
  <bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />

  <!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
  <bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
    <constructor-arg value="https://localhost:8443/cas/logout" />
    <constructor-arg>
      <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
    </constructor-arg>
    <property name="filterProcessesUrl" value="/logout/cas" />
  </bean>

  <security:http entry-point-ref="casEntryPoint" access-decision-manager-ref="accessDecisionManager" use-expressions="false">
    <security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
    <security:form-login />
    <security:logout />
    <security:custom-filter before="LOGOUT_FILTER" ref="requestSingleLogoutFilter"/>
    <security:custom-filter before="CAS_FILTER" ref="singleLogoutFilter"/>
    <security:custom-filter position="CAS_FILTER" ref="casFilter" />
  </security:http>

  <security:user-service id="userService">
    <security:user name="jimi" password="jimi" authorities="ROLE_ADMIN" />
    <security:user name="bob" password="bob" authorities="ROLE_USER" />
  </security:user-service>

  <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
    <property name="authenticationUserDetailsService">
      <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <constructor-arg index="0" ref="userService" />
      </bean>
    </property>
    <property name="serviceProperties" ref="serviceProperties" />
    <property name="ticketValidator">
      <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
        <constructor-arg index="0" value="https://localhost:8443/cas" />
      </bean>
    </property>
    <property name="key" value="localCAS" />
  </bean>

  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider ref="casAuthenticationProvider" />
  </security:authentication-manager>

  <int:channel-interceptor order="99">
    <bean class="org.springframework.integration.security.channel.SecurityContextPropagationChannelInterceptor"/>
  </int:channel-interceptor>

  <task:executor id="pool" pool-size="5"/>

  <int:poller id="poller" default="true" fixed-rate="1000"/>

  <int-security:secured-channels>
    <int-security:access-policy pattern=".*" send-access="ROLE_ADMIN" />
  </int-security:secured-channels>

  <int-http:inbound-channel-adapter path="/query*" supported-methods="GET, POST" channel="requestChannel" />

  <int:channel id="requestChannel">
    <int:queue/>
  </int:channel>

  <int-http:outbound-channel-adapter url="http://localhost:8080/prototype-integration-security-web/admin/callback?ticket={ticket}"
                                     http-method="GET"
                                     rest-template="restTemplate"
                                     channel="requestChannel">
    <int-http:uri-variable name="ticket" expression="T(org.springframework.security.core.context.SecurityContextHolder).context.authentication.credentials" />
  </int-http:outbound-channel-adapter>
</beans>

No additional code is required, this is why I am fond of Spring Integration. Did I do anything wrong or miss some configurations? Please share your ideas, opinions and suggestions. Thanks in advance.

Flik Shen
  • 27
  • 1
  • 10

1 Answers1

0

I have never used CAS before, but looks like you don't share how you get headers.serviceTicket.

I think you idea to propagate ticket via URL param is good, but first of all we have to extract it from the incoming URL:

Upon successful login, CAS will redirect the user’s browser back to the original service. It will also include a ticket parameter, which is an opaque string representing the "service ticket". Continuing our earlier example, the URL the browser is redirected to might be https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ.

http://docs.spring.io/spring-security/site/docs/4.2.0.RELEASE/reference/htmlsingle/#cas

For this purpose we can do like this:

<int-http:inbound-channel-adapter path="/user*" supported-methods="GET, POST" channel="userRequestChannel">
       <int-http:header name="serviceTicket" expression="#requestParams.ticket"/>
</int-http:inbound-channel-adapter>

Otherwise, please, share exception on the matter and try to trace network traffic to determine a gap.

UPDATE

According to the description on that Spring Security page for CAS we have:

... The principal will be equal to CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, whilst the credentials will be the service ticket opaque value...

So, looks like we don't need to worry about request param in the <int-http:inbound-channel-adapter> and just rely on the SecurityContext in the <int-http:outbound-gateway>:

<int-http:uri-variable name="ticket" 
         expression="T(org.springframework.security.core.context.SecurityContextHolder).context.authentication.credentials"/>
Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • Thank you for your suggestion. I tried as following and without luck got an exception said ticket is not public of MultipleValueMap which, I guess means ticket key-value pair is not there. ` ` Maybe the CasAuthenticationFilter has remvoed the ticket and I have to implement my own custom filter to hold ticket somewhere. – Flik Shen Dec 05 '16 at 03:42
  • Please, find an UPDATE in my answer. – Artem Bilan Dec 05 '16 at 16:31
  • Hi, Artem, many thanks for your suggestion and now I can get Service Ticket from SecurityContextHolder. However it doesn't work yet, my access is still denied by 403 error. It seems the backend service doesn't validate the ticket at all although i try to transfer the cookie by setting `transfer-cookie="true"` of outbound-gateway which replaces original outbound-channel-adapter of web module. – Flik Shen Dec 06 '16 at 11:36
  • I also tried to access urls `http://localhost:8080/prototype-integration-security-service/query?ticket=ST-3-e7vm3Jh13VeBKeLtmzL9-localhost` and `http://localhost:8080/prototype-integration-security-web/admin/callback` directly in my web browser, they return code 200 both. Maybe the restTemplate does not follow the HTTP redirect instructure completely. I google again and found [http://stackoverflow.com/questions/32392634/spring-resttemplate-redirect-302] talk about redirect then I change outbound-gateway to use HTTP GET Method then everything goes well. – Flik Shen Dec 06 '16 at 11:51
  • For HTTP POST method, I need dig more deep into `HttpComponentsClientHttpRequestFactory`. I think the prototype works so far. – Flik Shen Dec 06 '16 at 11:54