Friday, October 21, 2011

Apache CXF Web Services - tomcat - Basic Auth

Introduction

Web Services provide excellent means for communicating information between a provider of information and community of interest. With increasing popularity of SOA based implementations development and usage of web services has increased more than ever. However, web service implementers often ignore securing these communications. There are several ways to secure web service communication (for both SOAP and REST flavors). In this article, we describe usage of http basic auth for securing web services. For demonstration, we are utilizing Apache CXF framework, but the concepts can be applied over any other web service frameworks.  Additional mechanisms such as the WS-Security (SAML assertions, certificates, user id – password tokens) and application of SSL at the app server level will be discussed in a later article.

Basic Authentication

There are two ways to turn http style basic authentication on, only one is enough to secure web services:
1.       At application server level – Tomcat 6.0.32 will be used for this
2.       At End point level – Spring 3.0.5 / apache CXF 2.4.2 / Tomcat 6.0.32 will be used for this

Application Server Level

1.       Add the following to server.xml under <Engine> tag
a.       <Realm className="org.apache.catalina.realm.MemoryRealm"/>
2.       Add the following to tomcat-users.xml
a.       <role rolename="role1"/>
b.      <user username="role1" password="tomcat" roles="role1"/>
3.       In your webservice deployment descriptor (web.xml) file, add the following:
a.         <security-constraint>
b.          <web-resource-collection>
c.             <web-resource-name>
d.              EmployeeAuthService
e.            </web-resource-name>
f.         
g.             <url-pattern> /* </url-pattern>
h.            <http-method> GET </http-method>
i.               <http-method> POST </http-method>
j.            </web-resource-collection>
k.           <auth-constraint>
l.             <!-- the same like in your tomcat-users.conf file -->
m.          <role-name>role1</role-name>
n.          </auth-constraint>
o.        </security-constraint>
p.        
q.        
r.            <login-config>
s.          <auth-method>BASIC</auth-method>
t.         </login-config>
u.       
v.       <security-role>
w.         <description> Test role </description>
x.           <role-name> role1 </role-name>
y.         </security-role>

End Point Level

1.       Create a SoapHeaderInterceptor  in apache CXF as listed in the code below
package com.company.auth.authorizer.basicauth;

import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.cxf.binding.soap.interceptor.SoapHeaderInterceptor;
import org.apache.cxf.common.util.Base64Exception;
import org.apache.cxf.common.util.Base64Utility;
import org.apache.cxf.configuration.security.AuthorizationPolicy;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
import org.springframework.beans.factory.annotation.Required;

public class BasicAuthAuthorizationInterceptor extends SoapHeaderInterceptor {
               
                private Map<String, String> users;

                @Required
                public void setUsers(Map<String, String> users) {
                                System.out.println("Inside setUsers Map");
                                this.users = users;
                }

                @Override public void handleMessage(Message message) throws Fault {
                               
                                Map<String, List<String>> requestHeaders;
        requestHeaders = (Map<String, List<String>>) message.get(Message.PROTOCOL_HEADERS);
       
        List<String> authorizationLines = requestHeaders.get("authorization");
        List<String> aAuthorizationLines = requestHeaders.get("Authorization");
       
        if (authorizationLines != null)
        {
                System.out.println("Yay 1");
        }
                               
        if (aAuthorizationLines != null)
        {
                System.out.println("Yay 2");
        }

        // This is supposed to be set by CXF, but i had to set it explicitly, so don't
        // need this anymore
                                AuthorizationPolicy policy = message.get(AuthorizationPolicy.class);
       
        // If the policy is not set, the user did not specify credentials
        // A 401 is sent to the client to indicate that authentication is required
        if (policy == null) {
            System.out.println ("User attempted to log in with no credentials");
            sendErrorResponse(message, HttpURLConnection.HTTP_UNAUTHORIZED);
            return;
        }
       
                System.out.println ("Logging in use: " + policy.getUserName());
       
        // Verify the password
        String realPassword = users.get(policy.getUserName());
        if (realPassword == null || !realPassword.equals(policy.getPassword())) {
                System.out.println ("Invalid username or password for user: " + policy.getUserName());
            sendErrorResponse(message, HttpURLConnection.HTTP_FORBIDDEN);
        }
    }

                private void sendErrorResponse(Message message, int responseCode) {
        Message outMessage = getOutMessage(message);
        outMessage.put(Message.RESPONSE_CODE, responseCode);
       
        // Set the response headers
        Map<String, List<String>> responseHeaders =
            (Map<String, List<String>>)message.get(Message.PROTOCOL_HEADERS);
        if (responseHeaders != null) {
            responseHeaders.put("WWW-Authenticate", Arrays.asList(new String[]{"Basic realm=realm"}));
            responseHeaders.put("Content-Length", Arrays.asList(new String[]{"0"}));
        }
        message.getInterceptorChain().abort();
        try {
            getConduit(message).prepare(outMessage);
            close(outMessage);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


                private Message getOutMessage(Message inMessage) {
        Exchange exchange = inMessage.getExchange();
        Message outMessage = exchange.getOutMessage();
        if (outMessage == null) {
            Endpoint endpoint = exchange.get(Endpoint.class);
            outMessage = endpoint.getBinding().createMessage();
            exchange.setOutMessage(outMessage);
        }
        outMessage.putAll(inMessage);
        return outMessage;
    }


                private Conduit getConduit(Message inMessage) throws IOException {
                               
                                System.out.println("inmessage is: " + inMessage);
        Exchange exchange = inMessage.getExchange();
        if (exchange == null )
                System.out.println("Exchnage is null");
       
        EndpointReferenceType target = exchange.get(EndpointReferenceType.class);
        if (target == null )
                System.out.println("target is null");
       
        Conduit conduit =
            exchange.getDestination().getBackChannel(inMessage, null, target);
        exchange.setConduit(conduit);

        return conduit;
    }
   
    private void close(Message outMessage) throws IOException {
        OutputStream os = outMessage.getContent(OutputStream.class);
        os.flush();
        os.close();
    }

}

2.       Create Spring application configuration that is the standard for instantiating CXF web services as shown below:
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:jaxws="http://cxf.apache.org/jaxws"
      xmlns:http-conf="http://cxf.apache.org/transports/http/configuration"
      xmlns:sec="http://cxf.apache.org/configuration/security"
      xmlns:cxf="http://cxf.apache.org/core"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
     
http://www.springframework.org/schema/beans/spring-beans.xsd

http://cxf.apache.org/jaxws

                              http://cxf.apache.org/schemas/jaxws.xsd
                             
                              http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
                             
                                    http://cxf.apache.org/transports/http/configuration
     
           http://cxf.apache.org/schemas/configuration/http-conf.xsd
          
           http://cxf.apache.org/configuration/security
          
                http://cxf.apache.org/schemas/configuration/security.xsd"
                             
                              >

  <import resource="classpath:META-INF/cxf/cxf.xml" />
  <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/>
  <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
     
  <bean id="BasicAuthAuthorizationInterceptor" class="com.company.auth.authorizer.basicauth.BasicAuthAuthorizationInterceptor">
  <property name="users">
        <map>
          <entry key="synapseadmin" value="welcome1"/>
        </map>
      </property>
      </bean>         

  <jaxws:endpoint id="auth"
                  implementor="com.company.auth.service.AuthServiceImpl"
                  address="/cxfAuth">
                  <jaxws:inInterceptors>
        <ref bean="BasicAuthAuthorizationInterceptor"/>
      </jaxws:inInterceptors>
    </jaxws:endpoint>
 
 
 
</beans>

Client

In both cases the client code for calling web service can be written as below. Please note that client methods using both CXF jars and regular java net URL calls is used to demonstrate that basic auth parameters can be passed from various clients:
package com.company.auth.service.client;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Service;
import javax.xml.ws.handler.MessageContext;

import org.apache.cxf.configuration.security.AuthorizationPolicy;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.transport.http.HTTPConduit;


import com.company.auth.service.Employee;
import com.company.auth.service.AuthService;
import com.company.auth.service.EmployeeNotFoundException;
import com.company.auth.service.EmployeeNotFoundException_Exception;

public final class Client {

    private Client() {
    }

    public static void main(String args[]) throws Exception {

                method1();
                // method2();
               

    }
   
    public static void method1() {
                String ws_url = "http://localhost:8080/EmployeeAuthService/services/cxfAuth";
                JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();

                // factory.getInInterceptors().add(new BasicAuthAuthorizationInterceptor());
                // factory.getOutInterceptors().add(new BasicAuthAuthorizationInterceptor());
               
               
                factory.setServiceClass(AuthService.class);
                factory.setAddress(ws_url);
               
                // factory.setWsdlLocation("file:c://dev//junk//EmployeeAuthService.wsdl");
                // factory.setWsdlLocation(ws_url);
                AuthService client = (AuthService) factory.create();
                org.apache.cxf.endpoint.Client proxy = ClientProxy.getClient(client);
               
                //add username and password for container authentication
        BindingProvider bp = (BindingProvider) client;
        bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "synapseadmin");
        bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, "welcome1");
           
        /*
         *
        AuthorizationPolicy policy = new AuthorizationPolicy ();
        policy.setUserName("tomcat");
        policy.setPassword("tomcat");       
        bp.getRequestContext().put(AuthorizationPolicy.class.getName(), policy);
         */
       
        HTTPConduit conduit = (HTTPConduit) proxy.getConduit();
        AuthorizationPolicy auth = conduit.getAuthorization();
        if ( null == auth ) {
                System.out.println (" auth policy is null");
                auth = new AuthorizationPolicy();
        }
        auth.setUserName( "synapseadmin" );
        auth.setPassword( "welcome1" );
       
                com.company.auth.service.Employee employee;
                                                try {
                                                                employee = client.getEmployee("0223938");
                                                System.out.println("Server said: " + employee.getLastName() + ", " + employee.getFirstName());
                                                } catch (EmployeeNotFoundException_Exception e) {
                                                                // TODO Auto-generated catch block
                                                                e.printStackTrace();
                                                }
                System.exit(0);
               
    }
   
    public static void method2() {
                // String WS_URL = "http://localhost:8080/EmployeeAuthService/services/cxfAuth?wsdl";
                String WS_URL = "file:c://dev//junk//EmployeeAuthService.wsdl";
                 
    
                URL url = null;
                                try {
                                                url = new URL(WS_URL);
                                } catch (MalformedURLException e) {
                                                // TODO Auto-generated catch block
                                                e.printStackTrace();
                                }
                               
//                            {http://service.auth.company.com/}corporateAuthService

            QName qname = new QName("http://service.auth.company.com/", "corporateAuthService");
    
            Service service = Service.create(url, qname);
            AuthService port = service.getPort(AuthService.class);
    
            //add username and password for container authentication
            BindingProvider bp = (BindingProvider) port;
           bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "synapseadmin");
           bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, "welcome1");
    
                                                                try {
                                                                                System.out.println(port.getEmployee("0223938").getFirstName());
                                                                } catch (EmployeeNotFoundException_Exception e) {
                                                                                // TODO Auto-generated catch block
                                                                                e.printStackTrace();
                                                                }
    }

}

Server

The web services are developed using apache CXF and are listed below:

AuthService.java

package com.company.auth.service;

import javax.jws.WebService;
import javax.jws.WebParam;
import com.company.auth.bean.Employee;
import com.company.auth.service.exceptions.EmployeeNotFoundException;

@WebService
public interface AuthService {
    Employee getEmployee(@WebParam(name="gid") String gid) throws EmployeeNotFoundException;
    boolean isEmployee(@WebParam(name="emp") Employee emp);
}

AuthServiceImpl.java

package com.company.auth.service;

import javax.jws.WebService;

import com.company.auth.bean.Employee;
import com.company.auth.dao.EmployeeDAO;
import com.company.auth.service.exceptions.EmployeeNotFoundException;

@WebService(endpointInterface = "com.company.auth.service.AuthService", serviceName = "corporateAuthService")
public class AuthServiceImpl implements AuthService {

                public Employee getEmployee(String gid) throws EmployeeNotFoundException {
                                EmployeeDAO dao = new EmployeeDAO();
                                return dao.getEmployee(gid);
                }

                public boolean isEmployee(Employee emp) {
                                EmployeeDAO dao = new EmployeeDAO();
                                Employee emp2 = new Employee();
                               
                                try {
                                                emp2 = dao.getEmployee(emp.getGid());
                                                if (emp2.getFirstName() == emp.getFirstName())
                                                                return true;

                                } catch (EmployeeNotFoundException e) {
                                                // TODO Auto-generated catch block
                                                e.printStackTrace();
                                }
                                                               
                                return false;
                }

}

EmployeeDAO.java

package com.company.auth.dao;

import java.util.HashSet;
import java.util.Set;

import com.company.auth.bean.Employee;
import com.company.auth.service.exceptions.EmployeeNotFoundException;

public class EmployeeDAO {

                private Employee employee = new Employee();

                public  EmployeeDAO() {
                                employee.setFirstName("john");
                                employee.setLastName("smith");
                                Set privileges = new HashSet();
                                privileges.add("tea boy");
                                employee.setPrivileges(privileges);
                                employee.setGid("0223938");
                }

                public Employee getEmployee(String gid) throws EmployeeNotFoundException {
                                if (gid.equals("0223938"))
                                                return employee;
                                else
                                                throw new EmployeeNotFoundException("Employee is not found: " + gid);
                }

}

Employee.java

package com.company.auth.bean;

import java.io.Serializable;
import java.util.Set;

public class Employee implements Serializable {

                private static final long serialVersionUID = 1L;
                private String gid;
                private String lastName;
                private String firstName;
                private Set<String> privileges;

                public Employee() {}

                public Set<String> getPrivileges() {
                                return privileges;
                }

                public void setPrivileges(Set<String> privileges) {
                                this.privileges = privileges;
                }             

                public String getFirstName() {
                                return firstName;
                }

                public void setFirstName(String firstName) {
                                this.firstName = firstName;
                }

                public String getGid() {
                                return gid;
                }

                public void setGid(String gid) {
                                this.gid = gid;
                }

                public String getLastName() {
                                return lastName;
                }

                public void setLastName(String lastName) {
                                this.lastName = lastName;
                }

                public boolean isUserInRole(String role) {
                                if(privileges == null) { return false; }
                                else { return privileges.contains(role); }
                }

}

EmployeeNotFoundException.java

package com.company.auth.service.exceptions;

public class EmployeeNotFoundException extends Exception {
     
      /**
       *
       */
      private static final long serialVersionUID = 1L;

      public EmployeeNotFoundException(String string) {
            super(string);
      }

}