Fun with JWT in Salesforce
Adapted image from Tsahi Levent-Levi (https://www.flickr.com/photos/86979666@N00/8692704103/)

Fun with JWT in Salesforce

SalesforcePetr
5 min readJun 2, 2020

--

In the last couple of projects, I spent time integrating external systems with Salesforce CRM using Connected Apps in different set-ups and various flavors of OAuth. I’d like to share the fun I had with JWT.

Configuration of JWT-enabled Connected App

Enabling OAuth for Connected Apps is probably the most common use for Connected Apps. When aiming for server-to-server integration, JWT OAuth flow can be a great fit.

JWT Principle

As a reminder, the principle of JWT OAuth flow is extremely simple:

  1. Generate a signed JWT and swap it for an access token in Salesforce
  2. Use the access token authorizing your API requests to Salesforce until it’s no longer valid; then go to step 1

However, here’s a few fun facts that can trip you up when configuring JWT for a Connected App:

  • Callback URL isn’t used with JWT, but as a mandatory field it requires a value. Enter https://localhost.
  • Refresh tokens aren’t used with JWT, however, you need to specify the refresh_token / offline_accessscope. Otherwise an error of “refresh_token scope is required and the connected app should be installed and preauthorized” during the JWT flow.
  • IP relaxation has no impact, Salesforce always enforces the connecting user’s profile IP restrictions regardless of the Connected App’s relaxation policy.
  • Digital certificate’s expiration date isn’t enforced. This is by design, at least as of Spring ‘20.
  • There’s no authorization step for the end user within the JWT flow, users should be pre-authorized to use the JWT flow → create a marking profile or permission set and set the Connected App’s permitted users to these.

Digital Certificate

A digital certificate uploaded to a Connected App is essentially a public key, enriched by additional details, that can be used to verify a signature made by a corresponding private key held by the external system.

It’s perfectly fine to create a digital certificate on your own.

This command creates a digital certificate with a validity of one year.

Enter values identifying your external system as needed. They’re only visible on the Connected App’s set-up screen.

If you don’t have OpenSSL on your Windows machine, download it from https://www.cloudinsidr.com/content/how-to-install-the-most-recent-version-of-openssl-on-windows-10-in-64-bit/ (light version is enough).

Upload cert.pem as a digital certificate to your OAuth-enabled Connected App. It starts with -----BEGIN CERTIFICATE-----, whereas the private key starts with -----BEGIN PRIVATE KEY-----. Store the private key in a safe place, because it acts as a password.

Create JWT

Depending on the technology of the external system, choose a library to save your time. As a Groovy fan, I wrote a quick Groovy script relying on the com.auth0:java-jwt and org.bouncycastle:bcprov-jdk15on libraries:

The script produces a JWT signed by the private.pem private key.

The library needs a public.key for internal signature verification. You can generate it via:

Important
sub must reference a username, not federation id as of Spring ‘20

aud must be https://login.salesforce.com or (https://test.salesforce.com) but not MyDomain

Swap JWT for Access Token

You need to swap the JWT for an access token. Salesforce confirms validity of the request by cryptographically validating the signature using the digital certificate in the OAuth-enabled Connected App.

Make a POST request to /services/oauth2/token with:

  • grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
  • assertion=JWT value

You can make the request to https://login.salesforce.com (or https://test.salesforce.com) or MyDomain like here in Postman:

Exchanging JWT for Access Token

Important
The provided access_token will be recognized as authorization for your subsequent API requests at the instance_url. It’s typically your MyDomain even if you used https://login.salesforce.com where you POSTed the JWT to. Note that you’ll receive a “Destination URL not reset” error if you don’t do that.

Distribution of Connected Apps

Connected Apps have a very specific distribution model coming from these facts:

  • Client id and client secret are globally unique and can’t change;
  • Connected App has two set-up pages – definition (name, scope, etc.) and management (permitted users, etc.).
    You can adjust its definition only in the org where it’s defined, while you can adjust its management side separately in every org it’s been installed to.

Important
Define the Connected App in a production org.

Unless you’re exploring it, don’t define Connected App in a developer edition org since they’re subject to expiration. If you lose access to an org where the Connected App is defined, you can’t maintain its definition anymore.

Let’s differentiate between a single production org (deployments) and multiple productions orgs (packaging).

Deployments

Don’t put a Connected App’s metadata to a repository and don’t attempt to deploy it. Maintain your Connected App’s definition directly in the production org.

You can use it in your sandboxes by simply referencing a sandbox username (sub attribute), audience (aud), and URLs.

Packaging–Unlocked Packages

Connected Apps aren’t supported as of Spring ’20. You should document two manual steps to be done after your unlocked package is installed:

In the org with the installed package, navigate to URL /identity/app/AppInstallApprovalPage.apexp?app_id=0Ci… where 0Ci… is an id from the Connected App’s set-up definition page. This installs the Connected App. Then you need to configure the management side of it, e.g., who’s pre-authorized to use it.

Packaging–Managed Packages (2nd generation)

As of Spring ’20, you can get your Connected App (management side of) installed together with a 2nd generation managed package, but it entails awkward steps.

Important
Before a 2nd generation managed package can deliver a Connected App, that Connected App must be first part of a 1st generation managed package (it doesn’t have to be installed anywhere).

This is a bit tricky with the namespaces — so consider if you really need that. The logical next step for Salesforce is to fix an underlying problem that causes this manual step.

If you really really want to do it, here are the needed steps in detail:

  1. In a namespaced developer edition org registered with your DevHub org define a Connected App incl. setting OAuth scopes and uploading a digital cert.
  2. Create a 1st-generation managed package in that namespaced developer edition org.
  3. Copy the Connected App metadata file to your 2nd generation managed-package DX project folder, using the same namespace; include it in .forceignore to make it not pushable/deployable to scratch orgs.
  4. Finally, create a 2nd generation managed package.
  5. Install the 2nd generation managed package incl. the management side of the Connected App, however, its pre-authorized profiles or permissions set aren’t set even if you included them in the sources — so it’s again a manual step.

Recommendation
Don’t include Connected Apps in a 2nd-generation managed packages unless you’re a hard-core Salesforce enthusiast or before Salesforce fixes it.

Do you like this article? Follow me on Twitter, here on Medium (SalesforcePetr), or LinkedIn!

--

--

SalesforcePetr

Salesforce Solution Architect with some certifications