Making Authenticated Web Service Callouts Using Two-Way SSL

Abstract

Callouts is a powerful feature of the Force.com platform that allows you to connect to other web services and exchange data from inside Apex code or triggers. You can use this to notify other services of changes to data in your environment (org), or to retrieve data "on the fly" from a remote system and show it on a Visualforce page.

Callouts can be secured using SSL, as well as with two-way SSL, in which both the client and the server present certificates to prove their identity to each other. This article explains how you can use two-way SSL as a strong authentication method when you make callouts from Force.com to other services.

What is two-way SSL?

Normally when your browser establishes an SSL connection to a secure web site, only the server's certificate is checked. This "one-way" SSL mode is sufficient because the server does not rely on SSL to make assumptions about the browser client's identity. The browser on the other hand wants to check that the server is who it says it is. For example, when you connect to https://login.salesforce.com, the certificate will state that you have indeed reached "login.salesforce.com", so the browser knows it is talking to the right server. The server, login.salesforce.com, on the other hand, doesn't really care what kind of browser or computer you are using. All that matters to the server is that it can establish a secure communications channel to the other end that no one is listening in on. Then it can ask the user for credentials to prove identity.

In two-way SSL, both the client and server presents a certificate to prove their identity to the other party. Two-way SSL is supported by pretty much every SSL implementation out there and that makes it a great way to send user credentials from the client to the server. There's no need to understand or agree upon any additional security standards beyond just SSL.

In two-way SSL, the identities of the client and server are represented by digital certificates. The client and server do not have to exchange any kind of shared secret or perform other out-of-band communication. The trust between the two parties is established by having the certificates signed by a mutually trusted certificate authority.

Currently, Force.com only trusts certificates that have been signed by a commercial, trusted certificate authority (often referred to as a trusted CA or just a CA). Examples of such authorities are VeriSign and Thawte. This is the most common approach to establishing trust and all web servers come with documentation for how to create a certificate and have it signed by a CA.

How do I make my server trust a certificate from Force.com?

In your environment, you can create new certificates to be used for callouts. There are two ways you can configure your server to trust these certificates. Either you can download the certificate itself from the certificate and key management setup page and add it to the list of certificates trusted by your server or you can have the certificate signed by a certificate authority (CA) trusted by your server. This article will focus on the latter which is the most common approach.

Two-way SSL in action: A Sample Scenario

Let's now look at how to put this all into action. Your company, Acme Corp wants to make it easy for sales reps to check invoice status inside an application running on Force.com. Invoices are managed by an on-premise enterprise application and the IT department has agreed to provide limited read-only access via an HTTP XML service as long as the service is properly secured. The security policy requires that the remote client authenticates itself with a certificate signed by an internal certificate authority.

In this article we will use a Java web application hosted on Apache Tomcat to represent the on-premise invoice service. The first tutorial will build out this application and run it without any security for testing purposes. Later tutorials will secure the application and configure Force.com to make authenticated requests using client certificates.

Tutorial #1: A simple web service without security

Let's create a simple web service in Tomcat, create an HTTP callout from Force.com to this web service, and display the result in a Visualforce page.

We'll use a very simple servlet deployed in Tomcat 6 to model the on-premise system. The servlet's doGet method will simply write an XML structure with a couple of invoices to the response stream. We create this servlet in a web app called "InvoiceService". The servlet itself is mapped to the URL "/list", so if you execute an HTTP GET on http://[hostname]:[port]/InvoiceService/list, you'll get the following back:

<?xml version='1.0' encoding='utf-8'?><invoices>
  <invoice>
    <invoice-number>23432</invoice-number>
    <order-number>1223</order-number>
    <contract-number>343</contract-number>
    <due-date>12/31/2009</due-date>
    <amount currency='USD'>459000</amount>
    <status>Paid</status>
  </invoice>
  <invoice>
    <invoice-number>23456</invoice-number>
    <order-number>1201</order-number>
    <contract-number>371</contract-number>
    <due-date>01/31/2010</due-date>
    <amount currency='USD'>76000</amount>
    <status>Paid</status>
  </invoice>
</invoices>

The simplest possible servlet code to produce this response would look like this:

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;

public class InvoiceService extends HttpServlet {
       
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        response.getWriter().println("<?xml version='1.0' encoding='utf-8'?><invoices>\n"+
                     "  <invoice>\n"+
                     "    <invoice-number>23432</invoice-number>\n"+
                     "    <order-number>1223</order-number>\n"+
                     "    <contract-number>343</contract-number>\n"+
                     "    <due-date>12/31/2009</due-date>\n"+
                     "    <amount currency='USD'>459000</amount>\n"+
                     "    <status>Paid</status>\n"+
                     "  </invoice>\n"+
                     "  <invoice>\n"+
                     "    <invoice-number>23456</invoice-number>\n"+
                     "    <order-number>1201</order-number>\n"+
                     "    <contract-number>371</contract-number>\n"+
                     "    <due-date>01/31/2010</due-date>\n"+
                     "    <amount currency='USD'>76000</amount>\n"+
                     "    <status>Paid</status>\n"+
                     "  </invoice>\n"+
                     "</invoices>");
    }
}

For the purpose of this post, we assume this Tomcat instance is running on api.acmecorp.com, port 8888.

Controller extension that retrieves invoices for an account

Let's create an Apex controller extension that can fetch invoices for a given account from the corporate invoice system:

public class InvoiceServiceAccountExtension {

  private final String serviceEndpoint = 
      'http://api.acmecorp.com:8888/InvoiceService/list';
  private final String certName = null;

  private final Account acct;

  public class Invoice {
    public Integer invoiceNumber {get;set;}
    public Integer orderNumber {get;set;}
    public Integer contractNumber {get;set;}
    public Date dueDate {get;set;}
    public Double amount {get;set;}
    public String status {get;set;}
 
    public void from_xml(dom.XmlNode node) {
      invoiceNumber = Integer.valueOf(node.getChildElement('invoice-number',null).getText());
      orderNumber = Integer.valueOf(node.getChildElement('order-number',null).getText());
      contractNumber = Integer.valueOf(node.getChildElement('contract-number',null).getText());
      dueDate = Date.parse(node.getChildElement('due-date',null).getText());
      amount = Double.valueOf(node.getChildElement('amount',null).getText());
      status = node.getChildElement('status',null).getText();
    }
  }
 
  public List<Invoice> invoices {
    get {
      if(invoices==null) {
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint(serviceEndpoint+'?id='+acct.id);
        req.setMethod('GET');
        if(certName!=null) {
          req.setClientCertificateName(certName);
        }
        HttpResponse res = h.send(req);
 
        if(res.getStatusCode()>299) {
          System.debug('ERROR: '+res.getStatusCode()+': '+res.getStatus());
          System.debug(res.getBody());
        } else {
          dom.Document doc = res.getBodyDocument();
          invoices = new List<Invoice>();
          for(dom.XmlNode node : doc.getRootElement().getChildElements()) {
            if(node.getName()=='invoice') {
              Invoice iv = new Invoice();
              iv.from_xml(node);
              invoices.add(iv);
            }
          }
        }
      }
      return invoices;
    }
    set;
  }
 
  public InvoiceServiceAccountExtension(ApexPages.StandardController stdController) {
    this.acct = (Account)stdController.getRecord();
  }
}

This class first defines an inner class, "Invoice" to represent invoices. This inner class includes a convenience method to load data from an XML node. Then the class defines the "invoices" list property, including the code that retrieves invoices from the remote service. Right now, the endpoint is HTTP only with no SSL and we haven't configured a client certificate. Once we've built and tested the service we will move on to secure it.

In case you're wondering, the dom.XmlNode and dom.Document classes are new in Spring '10. They provide most of the functionality of a W3C DOM implementation to make it easier for you to process and generate XML documents in Apex. You can read more about it in the Apex Developer Guide Documentation.

Before you can connect to remote services of any kind from your Force.com organization, you have to create a remote site setting for the remote site:

Go to Setup -> Administration Setup -> Security Controls -> Remote Site Settings

Twowayyssl remotesites.png

Create a new site with the URL for your Tomcat Invoice service, e.g. http://api.acmecorp.com:8888. Choose any name/label you want

Twowwayssl remotesiteedit.png

Visualforce page to show account detail and related invoices

Now let's create a Visualforce page to show account detail including a related list of invoices. It uses the Apex controller extension we just wrote:

<apex:page standardController="Account" extensions="InvoiceServiceAccountExtension">
  <apex:detail subject="{!$CurrentPage.parameters.id}" relatedList="true" title="true"/> 
  <apex:pageBlock title="Invoices">
    <apex:pageBlockTable value="{!invoices}" var="iv">
      <apex:column value="{!iv.invoiceNumber}" headerValue="Invoice Number"/>
      <apex:column value="{!iv.orderNumber}" headerValue="Order Number"/>
      <apex:column value="{!iv.contractNumber}" headerValue="Contract Number"/>
      <apex:column value="{!iv.dueDate}" headerValue="Due Date"/>
      <apex:column value="{!iv.amount}" headerValue="Amount"/>
      <apex:column value="{!iv.status}" headerValue="Status"/>
    </apex:pageBlockTable>
  </apex:pageBlock>
</apex:page>

To make this page the standard detail page for accounts we'll customize the Account View button:

  1. Go to Setup -> Customize -> Accounts -> Buttons and Links
  2. In the "Standard Buttons and Links" section, click edit on the "View" entry
  3. Click the Visualforce page radio button and select the page you just created "Account Detail with Invoice" and click Save.

Test the new Account page with invoices

If you have followed along and got all the steps right, you should be able to test the service now. Log into your org, go to the Accounts tab and select one of the accounts in your org (from the recent list or by searching first).

The account detail page should look like the standard account detail page at the top:

Twowayssl acctdetail.png

If you scroll down, you should see the invoices related list at the bottom:

Twowayssl relatedinvoices.png

This data was retrieved from the Tomcat hosted invoice service when the page was rendered. If you've made it this far, you're now ready to configured a secured version of the service.

Tutorial #2: Tomcat "one-way" SSL configuration

Now that we have a simple Tomcat web service that we're calling out to, let's configure Tomcat to accept SSL connections using a signed server certificate. First, we must configure Tomcat to accept SSL connections. This task is already very well documented, so instead of repeating it all here read the Tomcat 6 SSL HOWTO and perform the following tasks:

  • Create a keystore with a new key pair for the Tomcat server certificate, put the keystore file in $HOME/tomcat/conf/tomcat.keystore and set the password to 'changeit' (assuming your tomcat directory is $HOME/tomcat)
  • Configure the SSL connector in server.xml
  • Create a certificate signing request for the new key pair
  • Have it signed by a public CA such as VeriSign
  • Import the certificate reply from the CA into the Tomcat keystore.


After these steps, you should have a keystore file in $HOME/tomcat/conf/tomcat.keystore with a valid, signed server certificate and you should have an SSL connector configuration like the following in your server.xml:

<Connector port="8443" minSpareThreads="5" maxSpareThreads="75"
 enableLookups="true" disableUploadTimeout="true" 
 acceptCount="100" maxThreads="200"
 scheme="https" secure="true" SSLEnabled="true"
 keystoreFile="${user.home}/tomcat/conf/tomcat.keystore" keystorePass="changeit"
 clientAuth="false" sslProtocol="TLS"/>

Make sure this part works before you proceed. You can use curl or your browser to check that Tomcat responds on https://[hostname]:8443 and successfully establishes a secure connection.

To test that this works, let's modify the account controller extension to connect using the SSL endpoint. Modify the value of the serviceEndpoint variable at the top of the class to:

 private final String serviceEndpoint = 
 'https://api.acmecorp.com:8443/InvoiceService/list';

Then go back to the remote site setting page and modify the Invoice Service URL accordingly

Twowayssl invoiceservice.png

Now reload the account detail page and verify that the related invoices still show up.

Tutorial #3: Securing access to the invoice service

Now that we have built the necessary application components for this scenario and tested that it works without and with basic SSL security, we move on to more advanced security. We will walk through the following steps:

  1. Specify that the invoice list resource is protected and can only be accessed by a specific role called 'invoice-readonly' and that it uses SSL client certificates as login method
  2. Create your own Acme Corp private certificate authority using openssl and add the root certificate to Tomcat as a trusted certificate authority (optional step, you can use a commercial CA as well)
  3. Create a new client certificate in Salesforce.com and have it signed by your Acme Corp CA
  4. Tweak the Force.com code and config to use SSL connection and send client certificate
  5. Create a new user in Tomcat's local, file based user store identified by the client certificate and map this user to the invoice-readonly role

Let's start!

Secure the invoice service web application

As a first step, we lock down access to the invoice service web application so only authenticated users in a certain role have access.

This is done in the web.xml file in the InvoiceService/WEB-INF directory by adding the following directives:

<security-constraint>
  <web-resource-collection>
    <web-resource-name>Invoice List</web-resource-name>
    <url-pattern>/list</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>invoice-readonly</role-name>
  </auth-constraint>
</security-constraint>
<login-config>
  <auth-method>CLIENT-CERT</auth-method>
  <realm-name>Invoice Service</realm-name>
</login-config>
<security-role>
  <role-name>invoice-readonly</role-name>
</security-role>

This basically says that the /InvoiceService/list resource can only be accessed by users in the invoice-readonly role. It also states that this application expects users to be authenticated using the CLIENT-CERT method, i.e. by presenting a client certificate as part of the SSL handshake.

To test it out, try to refresh your Account detail page (wait about 10 seconds to let Tomcat redeploy the app). The invoices related list should now be empty and an error message is printed in the system log if you have it open.

Set up a private certificate authority

The client certificate sent by Force.com will need to be signed by a certificate authority trusted by your server (Tomcat). You can have this certificate signed by a public CA just like your server certificate was, and then you can skip this step. But a private certificate authority can be a useful approach, so we will cover it here.

You will need OpenSSL installed somewhere (doesn't have to be on your server and it's actually better to keep your CA in a safer location). OpenSSL already comes with a CA setup, but we will create a new one, so you are not dependent on the default setup. For optimal security, you should perform all these actions as root and keep all files root owned and only accessible by root.

1. Create a directory for your CA:

mkdir myca 

2. Copy the default OpenSSL configuration file to your directory:

cp /etc/ssl/openssl.cnf myca/.
(location may vary, this is where it's located on my ubuntu 9 system, use locate  openssl.cnf for assistance)

3. Create some additional directories in the myca directory

cd myca
mkdir inbox
mkdir outbox
mkdir newcerts
mkdir private
mkdir crl

4. Create an empty index.txt file and a serial number file:

touch index.txt
echo '01' > serial

5. Change the dir property in the [ CA_default ] section of the openssl.cnf file from "demoCA" to ".":

####################################################################
[ CA_default ]

dir             = .                     # Where everything is kept
certs           = $dir/certs            # Where the issued certs are kept
crl_dir         = $dir/crl              # Where the issued crl are kept
database        = $dir/index.txt        # database index file.

6. Generate your root certificate:

openssl req -config openssl.cnf -new -x509 -extensions v3_ca -keyout private/cakey.pem -out cacert.pem -days 1825
(if you're going to use this CA for anything important, you must guard the generated cakey.pem very carefully)

Your new CA is now ready for use. Again, if you're using it for anything important, keep all these files very private, particularly the private key cakey.pem.

Install your new CA's root certificate in Tomcat

To make Tomcat trust certificates signed by your brand new CA, you must add the root certificate to Tomcat's trust store. You have already created a key store for holding Tomcat's server key and certificate. But a trust store is different. It holds public certificates that Tomcat should trust. Fortunately, you can use the same file as both key store and trust store with no problem. First use Java's keytool to add the CA certificate to the keystore (assumes your in the myca directory):

keytool -importcert -trustcacerts -alias my_ca -keystore $HOME/tomcat/conf/tomcat.keystore -file cacert.pem

You'll be prompted for the keystore password that you chose earlier when you created your key store. Tomcat's default is 'changeit', but you may have chosen another password.

You must tell Tomcat that this key store is now also the trust store. Modify the SSL connector configuration in server.xml by adding trust store attributes:

<Connector port="8443" minSpareThreads="5" maxSpareThreads="75"
 enableLookups="true" disableUploadTimeout="true" 
 acceptCount="100" maxThreads="200"
 scheme="https" secure="true" SSLEnabled="true"
 keystoreFile="${user.home}/tomcat/conf/tomcat.keystore" keystorePass="changeit"
 clientAuth="false" sslProtocol="TLS" 
 trustStoreFile="${user.home}/tomcat/conf/tomcat.keystore" 
 trustStorePass="changeit"/>

At this point you've locked down Tomcat to only accept connections to the web service if they present a client certificate, and you've configured Tomcat to use a certificate authority of your own choosing (which can be used to trust client certificates). Now let's create a client certificate, sign it with your certificate authority, and configure Force.com to use it.

Create a signed client certificate in your Force.com environment

Now it's time to create a client certificate that will be sent when the callout is made from your Force.com org:

  1. Go to Setup -> Administrative Setup -> Security Controls -> Certificate and Key Management
  2. Click New CA-Signed Certificate
  3. Type 'Invoice Service Client Certificate' as label and 'invoice_service' as unique name
  4. The values for Common Name, Email Address, Company, Department, City, State and Country Code are more or less dictated by who will be signing your certificate. If you want VeriSign to sign it, they have requirements that these values match your real identity. Since you're signing it with your own private CA, you can choose your own policy for what to put in these fields. The combination of these values make up the "distinguished name" of the certificate holder and this name should be unique across all the certificate holders that may connect to your server. To make it easier to follow this scenario, type in the following:
Common Name: CRMApplication
Email Address: Leave empty
Company: Acme Corp
Department: Sales
City: San Francisco
State: CA
Country Code: US

You should see something like this: Twowayssl certkeys.png

When you click Save, Salesforce.com generates a new certificate with the information you provided and show a page with the certificate detail. You can now sign this certificate with your private CA:

1. Download a certificate signing request by clicking Download Certificate Signing Request on the certificate detail page

2. Save this file named 'invoice_service.p10' in the inbox folder you created for your CA in the previous section.

3. Sign this request by executing from the myca directory:

openssl ca -config openssl.cnf -policy policy_anything -out outbox/invoice_service.crt -infiles inbox/invoice_service.p10

4. Create a pkcs7 formatted file with the new certificate reply and your CA's root certificate:

openssl crl2pkcs7 -nocrl -certfile outbox/invoice_service.crt -certfile cacert.pem -out outbox/invoice_service.p7b

5. Back in your Force.com org, click Upload Signed Certificate and upload the invoice_service.p7b file.


If all goes well, you now have a signed client certificate in Force.com, ready to use.

Update the account controller extension class to use the client certificate

Edit certName variable at the top changing its value from null to 'invoice_service':

private final String certName = 'invoice_service';

If you read through the code, you'll see that the method setClientCertificateName on the HttpRequest will now be called with 'invoice_service' as argument. This method, added in the Spring '10 release, allows you to use client certificates created in the Certificate Management Setup Page for Apex callouts by referencing their unique name.

Add the CRMApplication user to Tomcat's user store

The final step is to add the CRMApplication user to Tomcat's user store and map this user to the invoice-readonly role. As mentioned earlier, all the values you selected when creating the client certificate in Force.com will make up the distinguished name (DN) of your identity. Tomcat uses the DN as the user name of the user, so we add the following to the tomcat-users.xml file in Tomcat's conf directory:

<role rolename="invoice-readonly"/>
<user username="CN=CRMApplication, OU=Sales, O=Acme Corp, L=San Francisco, ST=CA, C=US" 
  password="null" roles="invoice-readonly"/>

That's it!

If you restart Tomcat and try reloading the Account detail page, the invoices related list should still show up. But now the remote service is protected and only by sending the signed client certificate as part of the callout will the service accept the request.

Summary

This article showed how you can use callouts to show data from your own on-premise systems inside Salesforce.com. First we covered how to build the integration itself and then we covered the very important topic of security and showed how you can use two-way SSL to establish mutually trusted, secure connections between Salesforce.com and your own web services.

References