Announcement Announcement Module
Collapse
No announcement yet.
WS-Security integration? Page Title Module
Move Remove Collapse
This topic is closed
X
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • WS-Security integration?

    I've set up Spring Security in a CXF project and would like to use WS-Security for authentication. In other words, a Java client will authenticate using a WS-Security UsernameToken; the username and password should then be passed to Spring Security to perform actual authentication.

    I've been able to set up WS-Security in CXF for a service, I just don't understand how to make WS-Security act as an authentication manager/provider for Spring Security.

    Would anyone be able to provide some guidance?

    Thank you,
    - Dave

  • #2
    First solution: create custom CXF interceptor or extend WSS4JInInterceptor. Inject AuthenticationManager to it. Interceptor will gather username/password from headers, create UsernamePasswordAuthenticationToken, try to authenticate via AuthenticationManager, put authenticated token into security context. Method-based authorization.
    Code:
    SecurityContextHolder.getContext().setAuthentication(authentication);
    Second solution: create custom spring security filter which will manually parse WSS/SAOP headers from request, create AuthenticationToken, authenticate it via manager, put in context. This approach is not coupled to CXF and others. It could be useful if such filter would be in spring security itself

    Approach can be adapted to various WSS tokens, for example, for X.509 Token.
    Last edited by Andrei Tsibets; Dec 6th, 2008, 08:39 AM.

    Comment


    • #3
      Hi Andrei,

      Thank you for your response! I made a CXF interceptor (I assumed this would be the faster option; I'm on company time or I'd consider doing the other). I now have a basic thing working. Below is my CXF configuration file and CXF interceptor.

      Code:
      <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:aop="http://www.springframework.org/schema/aop"
          xmlns:jaxrs="http://cxf.apache.org/jaxrs"
          xmlns:jaxws="http://cxf.apache.org/jaxws"
          xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://cxf.apache.org/jaxrs
          http://cxf.apache.org/schemas/jaxrs.xsd
          http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
          http://www.springframework.org/schema/security
          http://www.springframework.org/schema/security/spring-security-2.0.1.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
          http://cxf.apache.org/jaxws
          http://cxf.apache.org/schemas/jaxws.xsd">
          
          <import resource="classpath:META-INF/cxf/cxf.xml" />
          <import resource="classpath:META-INF/cxf/cxf-extension-jaxrs-binding.xml"/> 
          <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/>
          <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
          
          <jaxws:endpoint id="auth" implementor="com.company.auth.service.AuthServiceImpl" address="/corporateAuth">
              <jaxws:inInterceptors>
                  <bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
                      <constructor-arg>
                          <map>
                              <entry key="action" value="UsernameToken" />
                              <entry key="passwordType" value="PasswordText" />
                              <entry key="passwordCallbackClass" value="com.company.auth.service.ServerPasswordCallback"/>
                          </map>
                      </constructor-arg>
                  </bean>
                  <bean class="com.company.service.web.auth.WSAuthenticationInterceptor">
                      <property name="authenticationManager" ref="authenticationManager"/>
                  </bean>
              </jaxws:inInterceptors>
          </jaxws:endpoint>
      
          <security:authentication-manager alias="authenticationManager"/>
          <security:authentication-provider user-service-ref="myUserDetailsService"/>
          <security:global-method-security secured-annotations="enabled"/>
          
          <bean id="springSecurityFilterChain" class="org.springframework.security.util.FilterChainProxy">
              <security:filter-chain-map path-type="ant">
                  <security:filter-chain pattern="/**" filters="contextIntegrationFilter"/>
              </security:filter-chain-map>
          </bean>
          <bean id="contextIntegrationFilter" class="org.springframework.security.context.HttpSessionContextIntegrationFilter" />
          
          <bean id="myUserDetailsService" class="com.company.service.web.auth.AuthenticationDetailsService"/>
      Code:
      package com.company.service.web.auth;
      
      import java.util.Vector;
      
      import org.apache.cxf.binding.soap.SoapMessage;
      import org.apache.cxf.interceptor.Fault;
      import org.apache.cxf.phase.AbstractPhaseInterceptor;
      import org.apache.cxf.phase.Phase;
      import org.apache.ws.security.WSConstants;
      import org.apache.ws.security.WSSecurityEngineResult;
      import org.apache.ws.security.WSUsernameTokenPrincipal;
      import org.apache.ws.security.handler.WSHandlerConstants;
      import org.apache.ws.security.handler.WSHandlerResult;
      import org.springframework.beans.factory.InitializingBean;
      import org.springframework.security.Authentication;
      import org.springframework.security.AuthenticationManager;
      import org.springframework.security.context.SecurityContextHolder;
      import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
      import org.springframework.util.Assert;
      
      public class WSAuthenticationInterceptor extends AbstractPhaseInterceptor<SoapMessage> implements InitializingBean {
      
          private AuthenticationManager authenticationManager;
      
          public WSAuthenticationInterceptor() {
      
              super( Phase.POST_STREAM );
          }
      
          public void afterPropertiesSet() throws Exception {
      
              // ensure the 2 objects we need are not null
              Assert.notNull( authenticationManager, "Authentication Manager should not be null!" );
          }
      
          public void handleMessage( SoapMessage message ) throws Fault {
      
              // get out the results from the message context
              Vector<WSHandlerResult> result = (Vector<WSHandlerResult>) message.getContextualProperty( WSHandlerConstants.RECV_RESULTS );
              for (WSHandlerResult res : result) {
      
                  // loop through security engine results
                  for (WSSecurityEngineResult securityResult : (Vector<WSSecurityEngineResult>) res.getResults()) {
      
                      int action = securityResult.getAction();
      
                      // determine if the action was a username token
                      if ( ( action & WSConstants.UT ) > 0 ) {
      
                          // get the principal object
                          WSUsernameTokenPrincipal principal = (WSUsernameTokenPrincipal) securityResult.getPrincipal();
      
                          Authentication authentication = new UsernamePasswordAuthenticationToken( principal.getName(), principal.getPassword() );
                          authentication = authenticationManager.authenticate( authentication );
      
                          if ( !authentication.isAuthenticated() ) {
                              System.out.println( "This user is not authentic." );
                              //throw new AuthenticationException( "This user is not authentic." );
                          }
      
                          SecurityContextHolder.getContext().setAuthentication( authentication );
                      }
                  }
              }
          }
      
          /**
           * @return the authenticationManager
           */
          public AuthenticationManager getAuthenticationManager() {
      
              return authenticationManager;
          }
      
          /**
           * @param authenticationManager the authenticationManager to set
           */
          public void setAuthenticationManager( AuthenticationManager authenticationManager ) {
      
              this.authenticationManager = authenticationManager;
          }
      
      }
      Using the above, you have to ensure that your ServerPasswordCallback handler always returns the password specified by the client authentication request. CXF's WSS4JInInterceptor will thereby always successfully authenticate the client; the WSAuthenticationInterceptor will then really authenticate the client using Spring Security. Here is my ServerPasswordCallback class:

      Code:
      package com.company.auth.service;
      
      import java.io.IOException;
      
      import javax.security.auth.callback.Callback;
      import javax.security.auth.callback.CallbackHandler;
      import javax.security.auth.callback.UnsupportedCallbackException;
      
      import org.apache.ws.security.WSPasswordCallback;
      
      public class ServerPasswordCallback implements CallbackHandler {
      
          public void handle( Callback[] callbacks ) throws IOException, UnsupportedCallbackException {
      
              WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
      
              // this seems ridiculous, but is necessary for passing authentication on to Spring-Security.
              // we're essentially bypassing CXF's WSS4JInterceptor by ensuring that the password callback always matches the client password.
              pc.setPassword( pc.getPassword() );
          }
      
      }
      Thank you again, Andrei!
      - Dave

      Comment


      • #4
        Hi, Dave.

        I tried to implement your example in one of my own projects, but ran into a snag when I tested it. The short version of the story is that my call to
        Code:
        message.getContextualProperty(WSHandlerConstants.RECV_RESULTS)
        in WSAuthenticationInterceptor.handleMessage returns null, so I'm not able to pull out the UsernameToken from the message.

        Since I'm new to all of this stuff, there's no telling what's causing my problem, but I'd really appreciate it if you could help me track it down. I'm using Spring 2.5.6 with CXF 2.1.4.

        Here's the SOAP request I tried to send (edit: generated automatically by SoapUI after lots of trial and error)
        Code:
        <soapenv:Envelope xmlns:per="http://foo.bar.com/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
           <soapenv:Header><wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:UsernameToken wsu:Id="UsernameToken-799830164" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><wsse:Username>user</wsse:Username><wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"/></wsse:UsernameToken></wsse:Security></soapenv:Header>
           <soapenv:Body>
              <per:doSomething/>
           </soapenv:Body>
        </soapenv:Envelope>
        Here's my endpoint definition:
        Code:
        <jaxws:endpoint id="fooService" implementor="#fooBean" address="/foo">
                <jaxws:serviceFactory>
                    <ref bean="ServiceFactoryBean" />
                </jaxws:serviceFactory>
                <jaxws:inInterceptors>
                    <ref bean="wsInInterceptor" />
                    <ref bean="wsAuthenticationInterceptor" />
                </jaxws:inInterceptors>
            </jaxws:endpoint>
        Here are the relevant bean definitions:
        Code:
        <bean id="wsInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
                <constructor-arg>
                    <map>
                        <entry key="action" value="UsernameToken" />
                        <entry key="passwordType" value="PasswordText" />
                        <entry key="passwordCallbackClass" value="com.mycompany.springsupport.cxf.security.auth.ServerPasswordCallback"/>
                    </map>
                </constructor-arg>
            </bean>
            <bean id="wsAuthenticationInterceptor" class="com.mycompany.springsupport.cxf.security.auth.WSAuthenticationInterceptor">
                <property name="authenticationManager" ref="authenticationManager"/>
            </bean>
            <bean id="webservicesPasswordCallbackHandler" class="org.springframework.ws.soap.security.wss4j.callback.SpringPlainTextPasswordValidationCallbackHandler">
                <property name="authenticationManager" ref="authenticationManager" />
            </bean>
        .
        Last edited by sworisbreathing; Jun 18th, 2009, 07:14 PM.

        Comment


        • #5
          Hi sworisbreathing,

          Sorry for the delay in my response!

          Have you tried logging the incoming/outgoing messages to your jaxws:endpoint? If not, it might help shed some light on what's happening. See "Enabling message logging using plain Spring bean elements" on this page: Apache CXF 2.0 User's Guide - Configuration.

          Some problems I encountered initially:
          • Ordering of elements in the SOAP message. If, for example, User came after Password, the message wouldn't be processed correctly.
          • Too many elements in the SOAP message. I believe I was including an extra element in my request header (one which the WSS4JInInterceptor wasn't expecting), which caused the WSS4JInInterceptor to treat the request as being invalid.

          That being said, your SOAP request looks correct... Have you tried putting a password value in the <Password> element in your SOAP request? I noticed your request has a value for User, but no value for Password.

          Have you tried creating a Java client which attempts to authenticate against your web service? I haven't used SoapUI, but instead found an example of a CXF WSS client somewhere online (sorry, can't find it at the moment).

          I can't access my company's code base right now, and it's hard to recall everything we discovered when implementing this ourselves, but I'll try to think of other issues we encountered and let you know if I come up with anything.

          Good luck!
          - Dave

          Comment


          • #6
            Thanks for the response, Dave. After much trial and error, I got it working, but it was close-of-business Friday and I hadn't gotten around to posting my working version yet.

            It turns out there are a lot of things I didn't understand about how CXF and WSS4J operate (especially with respect to the fact that each interceptor in the chain modifies the SoapMessage object).

            Here's what my version ended up looking like.

            The jaxws:endpoint declaration
            Code:
                <jaxws:endpoint id="mySecureService" implementor="#mySecureServiceImpl" address="/mySecureService">
                    <jaxws:serviceFactory>
                        <ref bean="ServiceFactoryBean" />
                    </jaxws:serviceFactory>
                    <jaxws:inInterceptors>
                        <ref bean="wsAuthenticationInterceptor" />
                    </jaxws:inInterceptors>
                </jaxws:endpoint>
            The authentication interceptor declaration:
            Code:
                <bean id="wsAuthenticationInterceptor" class="com.mycompany.springsupport.cxf.security.auth.WSAuthenticationInInterceptor">
                    <constructor-arg index="0">
                        <map key-type="java.lang.String" value-type="java.lang.Object">
                            <entry key="action" value="UsernameToken" />
                            <entry key="passwordType" value="PasswordText" />
                            <entry key="passwordCallbackClass" value="com.mycompany.springsupport.cxf.security.auth.ServerPasswordCallback" />
                        </map>
                    </constructor-arg>
                    <property name="authenticationManager" ref="authenticationManager"/>
                </bean>
            The authentication interceptor definition... notice that this is a bit different from yours in that it extends org.apache.cxf.ws.security.wss4j.WSS4JInIntercepto r instead of AbstractPhaseInterceptor

            Code:
            package com.mycompany.springsupport.cxf.security.auth;
            
            import java.util.Map;
            import java.util.Vector;
            import org.apache.cxf.binding.soap.SoapMessage;
            import org.apache.cxf.interceptor.Fault;
            import org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor;
            import org.apache.log4j.Level;
            import org.apache.log4j.Logger;
            import org.apache.ws.security.WSConstants;
            import org.apache.ws.security.WSSecurityEngineResult;
            import org.apache.ws.security.WSUsernameTokenPrincipal;
            import org.apache.ws.security.handler.WSHandlerConstants;
            import org.apache.ws.security.handler.WSHandlerResult;
            import org.springframework.beans.factory.InitializingBean;
            import org.springframework.security.Authentication;
            import org.springframework.security.AuthenticationManager;
            import org.springframework.security.context.SecurityContextHolder;
            import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
            import org.springframework.util.Assert;
            
            public class WSAuthenticationInInterceptor extends WSS4JInInterceptor implements InitializingBean{
                private AuthenticationManager authenticationManager;
                public WSAuthenticationInInterceptor(){
                    super();
                }
                public WSAuthenticationInInterceptor(Map<String,Object> properties){
                    super(properties);
                }
            
                @Override
                public void afterPropertiesSet() throws Exception {
                    Assert.notNull(getAuthenticationManager(), "Authentication manager must be set");
                    Assert.notNull(getProperties(),"Interceptor properties must be set, even if empty");
                }
            
                /**
                 * @return the authenticationManager
                 */
                public AuthenticationManager getAuthenticationManager() {
                    return authenticationManager;
                }
            
                /**
                 * @param authenticationManager the authenticationManager to set
                 */
                public void setAuthenticationManager(AuthenticationManager authenticationManager) {
                    this.authenticationManager = authenticationManager;
                }
            
                @Override
                public void handleMessage(SoapMessage message) throws Fault {
                    try {
                        super.handleMessage(message);
                        Vector<WSHandlerResult> result = (Vector<WSHandlerResult>) message.getContextualProperty(WSHandlerConstants.RECV_RESULTS);
                        if (result != null && !result.isEmpty()) {
                            for (WSHandlerResult res : result) {
                                // loop through security engine results
                                for (WSSecurityEngineResult securityResult : (Vector<WSSecurityEngineResult>) res.getResults()) {
                                    int action = (Integer) securityResult.get(WSSecurityEngineResult.TAG_ACTION);
                                    // determine if the action was a username token
                                    if ((action & WSConstants.UT) > 0) {
                                        // get the principal object
                                        WSUsernameTokenPrincipal principal = (WSUsernameTokenPrincipal) securityResult.get(WSSecurityEngineResult.TAG_PRINCIPAL);
                                        if (principal.getPassword()==null){
                                            principal.setPassword("");
                                        }
                                        Authentication authentication = new UsernamePasswordAuthenticationToken(principal.getName(), principal.getPassword());
                                        authentication = authenticationManager.authenticate(authentication);
                                        if (!authentication.isAuthenticated()) {
                                            System.out.println("This user is not authentic.");
                                        //throw new AuthenticationException( "This user is not authentic." );
                                        }
                                        SecurityContextHolder.getContext().setAuthentication(authentication);
                                    }
                                }
                            }
                        }
                    } catch (RuntimeException ex) {
                        Logger.getLogger(WSAuthenticationInterceptor.class).log(Level.ERROR, ex.getMessage(), ex);
                        throw ex;
                    }
                }
            
            
            }
            What's really important is the call to
            Code:
            super.handleMessage(message)
            at the beginning of my handleMessage() implementation. It turns out that org.apache.cxf.ws.security.wss4j.WSS4JInIntercepto r.handleMessage() is what populates the RECV_RESULTS contextual property (remember how I said earlier that the interceptor chain modifies the message?) With that call in place, I was able to generate an authentication token and from there, my access decision manager and voters did their thing and the whole authentication system fell into place

            Tested and verified with SoapUI for a user who had permissions and a user who did not. Both worked as expected.

            Comment


            • #7
              Hi sworisbreathing,

              Glad you figured it out! Your solution looks more correct than mine, should anyone else be reading this. I'm not sure why I extended AbstractPhaseInterceptor instead of WSS4JInInterceptor...

              - Dave

              Comment

              Working...
              X