Salesforce Mutual Authentication - Part 1: the Basics
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. 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
<?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
<?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
$ 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.
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.
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
I got a timeout on port 8443 when I mentioned it. Have you encountered this before and how did you resolve it?
Aaron - which host were you trying to connect to? As I mentioned in the article, when I was working on this,
login.salesforce.comwas not listening on 8443.
I followed all the steps but my curl keeps saying “Client certificate error: unable to get local issuer certificate” Need urgent help on this.
Hi Ashish - is the certificate chain rooted with a real CA, or is it a self-signed root certificate?
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?
Hi Kumar - you need to Generate a Self-Signed Certificate. ‘Mutual Authentication’ is for apps calling in to your org - you are writing a callout. Unfortunately, Salesforce is a bit confusing here.
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?
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.
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.
As far as I know, yes. I bought a GoDaddy cert so I could test this.
Mangesh - I’m not sure what your question is here. Did you get it working?
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?
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.
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?
Hi Vlad… Unfortunately, it’s not possible; you have to use a ‘real’ SSL server cert signed by a CA trusted by Salesforce:
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!
One amendment. The cert chain file being used for curl does include the RSA Private Key entry at the top.
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
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();
I am getting error :
401 Unauthorized with message : Request did not provide the required two-way TLS certificate
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!
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.
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?
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.
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?
Hi Bini - I left Salesforce 5 years ago. This might be a good question to ask on https://salesforce.stackexchange.com/
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!
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 *