Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save booleangate/30d345ecf0617db0ea19c54c7a44d06f to your computer and use it in GitHub Desktop.
Save booleangate/30d345ecf0617db0ea19c54c7a44d06f to your computer and use it in GitHub Desktop.
Salesforce OAuth 2.0 JWT Bearer Token Flow walk-through

Salesforce OAuth 2.0 JWT Bearer Token Flow Walk-Through

This document will walk you through how to create or configure a Salesforce application for use with JWT authentication. These configuration steps and the example code works as of Salesforce API version 42.0.

Prerequisites

Create an RSA x509 private key/certification pair

openssl req -x509 -sha256 -nodes -days 36500 -newkey rsa:2048 -keyout salesforce.key -out salesforce.crt

The private key (.key) will be used to sign the JWT claim generated by your code. The certificate (.crt) will be uploaded to Salesforce to validate your signed JWT assertions.

Salesforce Application creation

  1. Login to salesforce.
  2. Go to setup area (gear in the nav in the top right)
  3. In the side nav, go to Apps > App Manager
    1. Click New Connect App
    2. In the Basic Information section, populate the required fields. The values are for book keeping only and are not part of using the API.
    3. In the API (Enable OAuth Settings) section:
      1. Check Enable OAuth Settings
      2. Callback URL is unused in the JWT flow but a value is required nonetheless. Use "http://localhost/" or some other dummy host.
      3. Check Use digital signatures. Upload the salesforce.crt that was generated earlier.
      4. For Selected OAuth Scopes, add Access and manage your data (api) and Perform requests on your behalf at any time (refresh_token, offline_access)
    4. Click Save. If there are any errors, you have to re-upload salesforce.crt.
  4. On the resulting app page, click Manage.
    1. Click Edit Policies.
    2. In the OAuth policies section, change Permitted Users to Admin approved users are pre-authorized.
    3. Click Save.
  5. Back on the app page again, in the Profiles section, click Manage Profiles.
    1. On the Application Profile Assignment page, assign the user profiles that will have access to this app.

OAuth Access Configuration

To use the API, the RSA private key and the Consumer Key (aka client ID) from the Salesforce application are needed.

  1. The private key is the key that was generated in the Prequisite section above.
  2. To get the Salesforce application Consumer Key, do the following
    1. Login to salesforce.
    2. Go to setup area (gear in the nav in the top right)
    3. In the side nav, go to Apps > App Manager
    4. In the list, find the application that you created in the App Creation section above
    5. From the drop down in the application's row, click View
    6. The Consumer Key is in the API (Enable OAuth Settings) section.

Parting Tips

# pip install jwt cryptography requests
from datetime import datetime
import jwt
import time
import requests
# *** Update these values to match your configuration ***
IS_SANDBOX = True
KEY_FILE = 'salesforce.key'
ISSUER = 'the consumer key from your application'
SUBJECT = 'your-sf-user@email.tld'
# *******************************************************
DOMAIN = 'test' if IS_SANDBOX else 'login'
print('Loading private key...')
with open(KEY_FILE) as fd:
private_key = fd.read()
print('Generating signed JWT assertion...')
claim = {
'iss': ISSUER,
'exp': int(time.time()) + 300,
'aud': 'https://{}.salesforce.com'.format(DOMAIN),
'sub': SUBJECT,
}
assertion = jwt.encode(claim, private_key, algorithm='RS256', headers={'alg':'RS256'}).decode('utf8')
print('Making OAuth request...')
r = requests.post('https://{}.salesforce.com/services/oauth2/token'.format(DOMAIN), data = {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': assertion,
})
print('Status:', r.status_code)
print(r.json())
@nasko
Copy link

nasko commented Jan 30, 2019

Hi @booleangate,
Thanks indeed for this walkthrough!

I have a question though - how should I go about uploading the salesforce.key file for signing in the Salesforce org? In Certificate and Key Management I can see only these options:

  • Create Self-Signed Certificate
  • Create CA-Signed Certificate
  • Import from Keystore - in case that's what I should use, I don't know how to convert the salesforce.key file to a Java Keystore format.

Thanks!

@Omkarbajpai
Copy link

hey, nice blog. we have a similar blog which shows another aspect of the topic. check it out How to Generate JWT Token from Salesforce And a feedback is always appreciated.

@SFoskitt
Copy link

This may be a very noob question, but is it safe to assume the code was written for Python 3? Thank you for this walkthrough.

@booleangate
Copy link
Author

@SFoskitt yes it was.

@Psynbiotik
Copy link

FYI the pip install line should be:
pip install pyjwt cryptography requests

@kaifresh
Copy link

this guide is the only clear one on this subject on the whole internet. thanks @booleangate

@akomarovsky
Copy link

thank you very much. Really cleared it to me

@mikearthur7b0
Copy link

mikearthur7b0 commented May 7, 2020

  • As per comment from Psynbiotik above, I installed pyjwt from the command line and left line 1 above commented out.

    python -m pip install pyjwt cryptography requests

  • Don't change any of the 'jwt' references to 'pyjwt' - leave as in the file above.

  • Do not name your file as 'jwt.py' (as I did)!!! If you do, you will get error 'JWT: 'module' object has no attribute 'encode''. Even after 'Save As' some-other-name.py I still couldn't get the error to go away until I created a new file with a sensible name and pasted the code in.
    Thanks https://stackoverflow.com/questions/33198428/jwt-module-object-has-no-attribute-encode#34388281

@rbasw-mbrandel
Copy link

Thanks! What is the Callback URL normally used for? I'm looking for ducumentation on why its not needed with JWT.

@mikearthur7b0
Copy link

@rbasw-mbrandel - This resource explains each of the authorisation flows:
https://trailhead.salesforce.com/content/learn/modules/connected-app-basics/connected-app-basics-api-integration
Some of the flows include a callback step, but JWT Bearer Flow does not.

@joecharoensiri
Copy link

Thanks! I can go get some sleep now. :)

@leefranke
Copy link

leefranke commented Nov 11, 2020

Figured it out.

Step 3-iii-c is where you use the salesforce.crt file.

@ChrHan
Copy link

ChrHan commented Feb 15, 2021

On top of changing jwt to pyjwt on pip install, I encountered an error:

Traceback (most recent call last):
  File "example.py", line 29, in <module>
    assertion = jwt.encode(claim, private_key, algorithm='RS256', headers={'alg':'RS256'}).decode('utf8')
AttributeError: 'str' object has no attribute 'decode'

This can be fixed by removing .decode('utf8') on the assertion, from:

assertion = jwt.encode(claim, private_key, algorithm='RS256', headers={'alg':'RS256'}).decode('utf8')

to:

assertion = jwt.encode(claim, private_key, algorithm='RS256', headers={'alg':'RS256'})

Once this is removed, this code runs successfully 🎉

@GodenYao
Copy link

GodenYao commented Mar 1, 2021

anyone has encountered the issue that this whole code (after fixing 'pyjwt', removing 'decode('utf8')' ) only works with salesforce sandbox, after I switched to the production environment, it errors out with 'invalid assertion'.

@987a73ae7992294d
Copy link

anyone has encountered the issue that this whole code (after fixing 'pyjwt', removing 'decode('utf8')' ) only works with salesforce sandbox, after I switched to the production environment, it errors out with 'invalid assertion'.

Presumably you figured this out already, but you probably forget to set the sandbox boolean to false...

Copy pasting code is dangerous.

This is helpful since Salesforce's stuff is a lot less straight forward in the docs. Thanks, author!

@Josip9989
Copy link

OMG Thank you!!!

Just a note, if you get an error like this:
{'error': 'invalid_client', 'error_description': 'invalid client credentials'}
try setting IS_SANDBOX = False

@silex-alexis
Copy link

thanks a lot for this! posting the equivalent C# / .NET Core code below (using Flurl, JWT libs).
Inject the configuration into static properties, and consume with SalesforceJwtAuthenticator.GenerateAuthenticationJwt() + SalesforceJwtAuthenticator.GetAccessTokenAsync(jwt)

using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Flurl;
using Flurl.Http;
using JWT.Builder;
using NLog;

namespace SfSync.SalesforceApi
{
    /// Encapsulates the Salesforce JWT Authentication flow client implementation
    /// Official doc: https://help.salesforce.com/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5
    /// Walk-through gist: https://gist.github.com/booleangate/30d345ecf0617db0ea19c54c7a44d06f
    public static class SalesforceJwtAuthenticator
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        // Parameters injection of parameters used for connection / authentication
        internal static string LoginDomain { private get; set; } = "https://login.salesforce.com/"; // use test.salesforce.com for sandbox
        internal static string CertPemFilePath { private get; set; }
        internal static string KeyPemFilePath { private get; set; }
        internal static string ConsumerKey { private get; set; }
        internal static string Username { private get; set; }


        /// Generate the JWT token that will identify sfsync and can be exchanged for an access token
        public static string GenerateAuthenticationJwt()
        {
            var cert = LoadCertificate();
            var jwt = JwtBuilder.Create()
                .WithAlgorithm(new JWT.Algorithms.RS256Algorithm(cert))
                .AddClaim("iss", ConsumerKey)
                .AddClaim("exp", DateTimeOffset.UtcNow.AddMinutes(15).ToUnixTimeSeconds())
                .AddClaim("aud", LoginDomain) // other option is test.salesforce.com for sandbox
                .AddClaim("sub", Username)
                .Encode();
            return jwt;
        }

        /// Performs the JWT <--> Access Token exchange
        public async static Task<string> GetAccessTokenAsync(string jwt)
        {
            Logger.Info("(getaccesstokenasync.begin)");

            var response = await LoginDomain.AppendPathSegment("/services/oauth2/token")
                .PostUrlEncodedAsync(new {
                    grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer",
                    assertion = jwt
                }).ReceiveJson<TokenResponseDto>();

            Logger.Info($"(getaccesstokenasync.success) tokenlen={response.access_token?.Length}"
                + $" scope={response.scope} instance_url={response.instance_url} id={response.id}"
                + $" token_type={response.token_type}");
            return response.access_token;
        }

        /// Load Certificate (and private key) from PEM files + warn / error in certificate is close to expire / has expired
        private static X509Certificate2 LoadCertificate()
        {
            var cert = X509Certificate2.CreateFromPemFile(CertPemFilePath, KeyPemFilePath);
            if (cert.NotAfter <= DateTime.Now)
                Logger.Error($"(certificate.expiry.error) notAfter='{cert.NotAfter.ToString(System.Globalization.CultureInfo.InvariantCulture)}'"
                    + " msg='certificate has expired, check README.md for how to generate a new one'");
            if (cert.NotAfter.AddDays(-30) <= DateTime.Now)
                Logger.Warn($"(certificate.expiry.warning) notAfter='{cert.NotAfter.ToString(System.Globalization.CultureInfo.InvariantCulture)}'"
                    +" msg='certificate will expire in less than 30 days, check README.md for how to generate a new one'");
            return cert;
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment