Salesforce Mutual Authentication - Part 1: the Basics

5 minute read

Mutual Authentication was introduced by Salesforce in the Winter ‘14 release. As the Salesforce Winter ‘14 release notes explain, mutually authenticated transport layer security (TLS) allows secure server-to-server connections initiated by a client using client certificate authentication, and means that both the client and the server authenticate and verify that they are who they say they are. In this blog post, I’ll show you how to enable Mutual Authentication and perform some basic tests using the curl command line tool. In a future blog post, I’ll show you how to implement Mutual Authentication in your Java apps. In the default case, without Mutual Authentication, when an API client connects to Salesforce via TLS, the client authenticates the server via its TLS certificate, but the TLS connection itself gives the server no information on the client’s identity. After the TLS session is established, the client sends a login request containing its credentials over the secure channel, the Salesforce login service responding with a session ID. The client then sends this session ID with each API request. Mutual Authentication provides an additional layer of security. Each time you connect to a Salesforce API, the server checks that the client’s certificate is valid for the client’s org, as well as checking the validity of the session ID. Note that Mutual Authentication is intended for API use and not for user interface (web browser) use. Before you can use Mutual Authentication, you need to obtain a client certificate. This certificate must be issued by a certificate authority with its root certificate in the Salesforce Outbound Messaging SSL CA Certificates list; Mutual Authentication will not work with a self-signed client certificate. More information is available in the Salesforce document, Set Up a Mutual Authentication Certificate. I bought an SSL certificate from GoDaddy - you can almost certainly find a cheaper alternative if you spend some time looking.

Enabling Mutual Authentication in Salesforce

Mutual Authentication is not enabled by default. You must open a support case with Salesforce to enable it. When it is enabled, you will see a Mutual Authentication Certificates section at Setup | Administer | Security Controls | Certificate and Key Management. Mutual Authentication Configuration You must upload a PEM-encoded client certificate to this list. Note that you need only upload the client certificate itself; do not upload a certificate chain. You will also need to create a user profile with the Enforce SSL/TLS Mutual Authentication user permission enabled. Clone an existing Salesforce profile and enable Enforce SSL/TLS Mutual Authentication. Check that the profile has the Salesforce object permissions that your application will need to access data. Assign the new profile to the user which your app will use to access Salesforce.

Testing Mutual Authentication with curl

This was a stumbling block for me for some time. First, despite what the Salesforce documentation (Configure Your API Client to Use Mutual Authentication) says, the Salesforce login service does not support Mutual Authentication. You cannot connect to login.salesforce.com on port 8443 as described in the docs. You can, however, send a normal authentication request for a user with Enforce SSL/TLS Mutual Authentication enabled to the default TLS port, 443. The login service responds with a session ID as for any other login request. Mutual Authentication is enforced when you use the session ID with an API endpoint. Let’s try this out. Here’s a SOAP login request - add a username/password and save it to login.xml:

<?xml version="1.0" encoding="utf-8" ?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <n1:login xmlns:n1="urn:partner.soap.sforce.com">
      <n1:username>[email protected]</n1:username>
      <n1:password>p455w0rd</n1:password>
    </n1:login>
  </env:Body>
</env:Envelope>

Now you can send it to the login service with curl:

$ curl -s -k https://login.salesforce.com/services/Soap/u/41.0 \
    -H "Content-Type: text/xml; charset=UTF-8" \
    -H "SOAPAction: login" \
    -d @login.xml | xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Body>
    <loginResponse>
      <result>
        <metadataServerUrl>https://na30.salesforce.com/services/Soap/m/41.0/00D36000000bLGT</metadataServerUrl>
        <passwordExpired>false</passwordExpired>
        <sandbox>false</sandbox>
        <serverUrl>https://na30.salesforce.com/services/Soap/u/41.0/00D36000000bLGT</serverUrl>
        <sessionId>00D36000000bLGT!AQQAQMlp30Zpiy6_gSeP1cmQG.0dHlfMcUDg96d8BSRpSb9BwksAABdKsde14ahtDGzKzRXAMroiomST8.UWcg.hp5XXDi4O</sessionId>
        <userId>00536000006Z51jAAC</userId>
        <userInfo>
          ...lots of user data...
        </userInfo>
      </result>
    </loginResponse>
  </soapenv:Body>
</soapenv:Envelope>

We need to create a PEM file for curl with the signing key, client certificate, and all the certificates in its chain except the root. This file looks something like this:

-----BEGIN RSA PRIVATE KEY-----
...base 64 encoded private key data...
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
...base64 encoded client certificate data...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...base64 encoded CA issuing cert...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...another base64 encoded CA issuing cert...
-----END CERTIFICATE-----

We’ll call the getUserInfo API. Here’s the SOAP request - add the session ID returned from login and save it as getuserinfo.xml:

<?xml version="1.0" encoding="utf-8"?> 
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:urn="urn:partner.soap.sforce.com">
  <soapenv:Header>
    <urn:SessionHeader>
      <urn:sessionId>INSERT_YOUR_SESSION_ID_HERE</urn:sessionId>
    </urn:SessionHeader>
  </soapenv:Header>
  <soapenv:Body>
    <urn:getUserInfo />
  </soapenv:Body>
</soapenv:Envelope>

Now we’re ready to make a mutually authenticated call to a Salesforce API! You’ll need to specify the correct instance, as returned in the login response, in the URL. Note the port number is 8443:

$ curl -s -k https://na30.salesforce.com:8443/services/Soap/u/41.0 \
    -H "Content-Type: text/xml; charset=UTF-8" \
    -H "SOAPAction: example" \
    -d @getuserinfo.xml \
    -E fullcert.pem | xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Header>
    <LimitInfoHeader>
      <limitInfo>
        <current>6</current>
        <limit>15000</limit>
        <type>API REQUESTS</type>
      </limitInfo>
    </LimitInfoHeader>
  </soapenv:Header>
  <soapenv:Body>
    <getUserInfoResponse>
      <result>
        ...all the user data...
      </result>
    </getUserInfoResponse>
  </soapenv:Body>
</soapenv:Envelope>

Now let’s look at a couple of failure modes. What happens when we call the 8443 port, but don’t pass a client certificate?

$ curl -s -k https://na30.salesforce.com:8443/services/Soap/u/41.0 \
    -H "Content-Type: text/xml; charset=UTF-8" \
    -H "SOAPAction: example" \
    -d @getuserinfo.xml
<html><head><title>Certificate Error</title></head><body bgcolor=#ffffff text=#3198d8><center><img src="http://www.sfdcstatic.com/common/assets/img/logo-company.png"><p><h3>Client certificate error:<i>No client certificate provided.</i></h3></center></body></html>

Note the HTML response, rather than XML! What about calling the regular 443 port with this session ID?

$ curl -s -k https://na30.salesforce.com/services/Soap/u/41.0 \
    -H "Content-Type: text/xml; charset=UTF-8" \
    -H "SOAPAction: example" \
    -d @getuserinfo.xml
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sf="urn:fault.partner.soap.sforce.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Body>
    <soapenv:Fault>
      <faultcode>sf:MUTUAL_AUTHENTICATION_FAILED</faultcode>
      <faultstring>MUTUAL_AUTHENTICATION_FAILED: This session could not be mutually authenticated for use with the API</faultstring>
      <detail>
        <sf:UnexpectedErrorFault xsi:type="sf:UnexpectedErrorFault">
          <sf:exceptionCode>MUTUAL_AUTHENTICATION_FAILED</sf:exceptionCode>
          <sf:exceptionMessage>This session could not be mutually authenticated for use with the API</sf:exceptionMessage>
        </sf:UnexpectedErrorFault>
      </detail>
    </soapenv:Fault>
  </soapenv:Body>
</soapenv:Envelope>

This time we get a much more palatable response! Now you know how to get the basics of Salesforce Mutual Authentication working. In part 2 of this series, I look at using Salesforce’s Web Service Connector (WSC) to access the SOAP and Bulk APIs with Mutual Authentication, and in part 3, I explain how to access the Salesforce REST APIs with common Java HTTP clients such as the Apache and Jetty.

Updated:

Comments

Jitendra

Hi, Nice to read your article. its very detailed.

I need your help in Client Certificate. Please let me know how did you get the SSL Client Certificate from Godaddy . I tried a lot but didn’t get any information.

Thanks, Jitendra

Pat Patterson

Hi Jitendra - you need to generate a CSR (certificate signing request) - see the ‘Java’ instructions at https://www.godaddy.com/help/generate-a-csr-certificate-signing-request-5343

Pat

Aaron - which host were you trying to connect to? As I mentioned in the article, when I was working on this, login.salesforce.com was not listening on 8443.

Kumar

Hi, I am also working with login.salesforce.com I have the certificate which is issued by ADP . I have inserted by certificate in salesforce under Mutual Authentication but when i tried to access certificate in my HTTP Request using req.setClientCertificateName(‘ADP’); I got an error : System.CalloutException: Could not find client cert with dev name: ‘ADP’ Please suggests what am i missing?

Aneesh

Hi Pat,

I had a very basic question. When you say the certificate should be signed by a Saleforce trusted Root CA, you mean we need to buy one even to try MTLS on sandboxes?

Mangesh

Hi,

We got a CA Signed Certificate from the Client Target Host.

We have uploaded it in Mutual Authentication.

Do we also have to share Self-Signed with the Client Target Host now?

We thought it will automatically generate a Self-Signed one.

Mangesh

https://help.salesforce.com/articleView?id=000326722&type=1&mode=1

In this document, we are making call-out.. and it says we need CA-Signed Certificate from Target Host.

I mean, basically we were not allowed to make call-out to a system inside the firewall. Client gave us a Public CA-signed Certificate.

Since the CSR was not given by us, we enabled Mutual SSL as per this document.

Prem

Hi Pat,

In this article we see a statement - “We need to create a PEM file for curl with the signing key, client certificate, and all the certificates in its chain except the root.”.

From where do we get this file?

Pat

Hi Prem! You have to create a signing key and submit the public key to one of the CA’s trusted by Salesforce. You CANNOT use a self-signed certificate. The details vary according to which CA you use. I’ve used GoDaddy in the past - their instructions are here. You generate the key and certificate signing request (CSR), keep the key (it’s your secret!) and submit the CSR to the CA. They will issue the certificate as a PEM file that you can download. NOTE - you need an SSL certificate, not a code-signing certificate.

Vlad

Fully support your preference on the Scotch side ;-) !

This is all well explained, but this is for the case when Somebody externally trying to reach Salesforce.

But would you know if it is possible in an opposite direction? When we have an external End Point and we shared our Certificate with the server side, and we have received a Server certificate.

I see no place to save it, at least not on the Named Credential side.

This article is written to pretend it is two-way ssl but it describes only the client certificate (salesforce) on how to sign requests: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts_client_certs.htm?search_text=two%20way%20ssl

I need to understand if this is possible, and if it is, on where should I save Server side certificate in the salesforce, and make sure my code validates the end point against it?

Thanks, Vlad

Pat

Hi Vlad… Unfortunately, it’s not possible; you have to use a ‘real’ SSL server cert signed by a CA trusted by Salesforce:

When sending outbound messages, delegated authentication requests or Apex callouts to secure/SSL endpoints (e.g. https://myintegration.acme.com), a Salesforce.com organization (acting as the client) will only trust the target host (that will act as the server) if this presents a certificate signed by a root Certification Authority (CA) [...]. In other words, in this scenario self-signed certificates are not allowed to be used by the target host.

See https://help.salesforce.com/articleView?id=000326722&type=1&mode=1

David T

Hi Pat. I feel like every time I have to do something with certificates I have to re-learn it.

Does the CSR have to be generated on the SF org, i.e. can I upload a client cert generated based on another org’s CSR?

I’m getting “Client certificate error: unable to get local issuer certificate”.

I uploaded the client certificate (without any chain). I have the RSA private key. I created the cert chain (the client head, digicert intermediate only). The root cert is left out, and was verified by thumbprint: MD5:79:E4:A9:84:0D:7D:3A:96:D7:C0:4F:E2:43:4C:89:2E

I am past the login step - I’m trying to execute the getUserInfo call against :8443 of my org.

Any pointers? Thanks!

Pat

Hi David - I can totally relate on the relearning side. One of the reasons I write blog entries like this is so I can refer back to them next time. I can’t count the number of times I’ve googled a problem and found the answer written by 2-years-ago-me :-)

As far as I know, you have to generate the private key yourself. You submit the corresponding public key to a CA to get a cert chain rooted at one of the root CA certs that Salesforce trusts. That bit is documented at https://help.salesforce.com/s/articleView?id=sf.security_keys_about.htm&type=5

Looking at the error you’re seeing - this seems to come from curl itself rather than the Salesforce side. It may be that the curl you’re using isn’t happy with the root CA cert it’s getting from Salesforce, because it doesn’t have the current root cert. I found this Stack Overflow answer that seems relevant: https://stackoverflow.com/a/31830614/33905

Good luck!

Bini

Hi Kumar, Were you able to resolve your issue. I am facing the exact same issue - Connect from Salesforce using ADP cert to REST API through Marketplace. ======= Here are some of the details: I am trying to implement REST API callout from salesforce. I am given a certificate (xyz.CER file and private key) from REST API application to use for mutual authentication. (I use this cer and key file in Postman to invoke API and it works fine)

How do I upload and use this certificate and key in Salesforce?

Here is what I tried:

1) In setup, I went to “Certificate and Key Management” -> Upload Mutual Authentication Certificate”

and uploaded xyz.cer file. I CANNOT upload KEY file. It allows only one file.

I tried to use ‘labelname’ in code as below screenshot:

HttpRequest req = new HttpRequest();

req.setClientCertificateName(‘labelname’);

I am getting error :

401 Unauthorized with message : Request did not provide the required two-way TLS certificate

2)

I combined cer file and key file as suggested below in PEM file, but this didn’t work either.

Salesforce is not recognizing it as certificate. I didn’t have root certificate in this chain. Wondering if that will fix the issue.

—–BEGIN RSA PRIVATE KEY—– (Your Private Key) —–END RSA PRIVATE KEY—– —–BEGIN CERTIFICATE—– (Your Primary SSL certificate) —–END CERTIFICATE—–

Any help is appreciated!

Thanks,

Pat

Hi Bini - as I mentioned in my reply to Kumar, this area of Salesforce is quite confusing, and not well documented. Mutual Authentication certificates are used by Salesforce to verify clients calling the Salesforce APIs. The client uses its private key in the TLS handshake and Salesforce verifies it against the certificate chain you uploaded.

You are in the opposite situation - you want to load a private key and certificate chain into Salesforce so that when your Apex code does a callout, Salesforce can use the private key in the TLS handshake and ADP can verify it against the certificate it has on record. I believe you need to create a keystore in Java Keystore (JKS) format containing your private key and certificate chain and import it into Salesforce using the Import from Keystore button in the Certificates section of the Certificate and Key Management page.

Leon

Hey Pat, thank you very much for this blog. I got one question regarding IP whitelisting. Say I have a non-public external webservice, which Salesforce can consume as a client. Do I still need to whitelist the Salesforce IP using mutual TSL? Meaning, is mutual TSL built on top of IP whitelisting?

Pat

Hi Leon - all outbound connections from your Salesforce org must be configured in either Remote Site Settings or Named Credentials - see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts_remote_site_settings.htm

Mutual authentication is independent of those settings - you have to configure both.

IP whitelisting is for inbound connections.

Bini

Hi Pat, I am trying to implement two way ssl in Callout. I am trying to generate keystore and import keystore. Will that work? Need some clarification on to generate and upload keystore in Salesforce and use it during callout. Can you point in the right direction?

Thanks Bini

Mike M

Hi Pat, a couple other folks have mentioned the “Client certificate error: unable to get local issuer certificate” error on this article. See screenshot here for the error: https://www.screencast.com/t/tVsETEHVfee

I found your article because I searched for that error in Google with quotation marks around it, and this article is the only result. 1 single result!

This happens when trying to setup Certificate-Based authentication in Salesforce when using Chrome on a Mac. It works fine on Chrome on Windows, but not Chrome on a Mac. It works fine on Safari on a Mac, but not Chrome. I have tried from multiple Mac computers using multiple versions of MacOS, and the problem is the same – fine on Safari, broken on Chrome.

Do you have any tips for me? Thanks!

Pat

Hi Mike - apologies for the delay - your comment was in my moderation queue over the holidays. I’m afraid I don’t have any tips - I was doing client cert-based authentication from an app, and it looks like you’re trying to do it in the browser. Is that correct?

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...