From the Trenches: Authenticate without user interaction via a system user

Automating business processes is becoming a need for every organization, and that’s why DocuSign provides JSON Web Token (JWT) Grant authentication as one of the OAuth 2.0 authentication flows. Using JWT enables organizations to build system integrations with DocuSign by using a system user, eliminating the need for user interaction to input credentials.

Possible scenarios

When using JWT Grant, you have two possible scenarios to impersonate a user to be able to call DocuSign APIs:

  1. Using a single system user, as I will explain in this blog post.
  2. Using the “Send on Behalf Of” feature as explained in a previous blog post, From the Trenches: OAuth 2.0 and the new SOBO.

Prerequisites

To use JWT Grant authentication with a system user, you must meet the following prerequisites:

  • You have defined an integration key.

    An integration key identifies your integration and links to its configuration values. To create an integration key:

    1. Open the Apps and Keys page.
    2. Under My Apps / Integration Keys, select Add App / Integration Key, then give it a name.
  • You have defined a redirect URI for your integration key.

    The redirect URI is the URI (URL) to which DocuSign will redirect the browser after authentication. In the case of JWT Grant, this is only used during the consent process, as I explain later on. To define a redirect URI:

    1. Open the Apps and Keys page.
    2. Under My Apps / Integration Keys, choose the integration key to use, then select Actions, then Edit.
    3. In the Additional Settings section, select Add URI.
    4. Enter the new redirect URI. This can be a localhost address.
  • Your application has an RSA key pair.

    To add an RSA key pair to your application:

    1. Open the Apps and Keys page.
    2. Under My Apps / Integration Keys, choose the integration key to use, then select Actions, then Edit.
    3. In the Authentication section, select Add RSA Keypair.
    4. Save both the public and private keys to a secure place, including the ----BEGIN RSA PRIVATE KEY---- and ----END RSA PRIVATE KEY----- lines as part of the text.

Step 1: Request application consent

Before using the system user with the DocuSign APIs, you need to grant consent for your integration to impersonate that system user. This will be a one-time manual step through your browser that you will not need to do again unless someone manually revokes that consent.

To grant consent, you need to construct a URI like the one below.

https://YOUR_AUTH_BASE_PATH/oauth/auth?
    response_type=code
    &scope=YOUR_REQUESTED_SCOPES
    &client_id=YOUR_INTEGRATION_KEY
    &redirect_uri=YOUR_REDIRECT_URI

Just replace the placeholders with their respective values:

YOUR_AUTH_BASE_PATH For the eSignature REST API, use the following: 
  • For the developer environment, it should be account-d.docusign.com
  • For the production environment, it should be account.docusign.com
For other APIs, see DocuSign API endpoint base paths.
YOUR_REQUESTED_SCOPES A space-delimited list of scopes your integration needs. The basic scopes needed for eSignature are signature and impersonation. For a full list of available scopes per DocuSign API, see Authentication scopes.
YOUR_INTEGRATION_KEY Your integration key.
YOUR_REDIRECT_URI A URL-encoded redirect URI that matches one of the redirect URIs configured for the integration key being used.

An example of the consent URI would look like this:

https://account-d.docusign.com/oauth/auth?
    response_type=code
    &scope=signature%20impersonation
    &client_id=d3xxxx6f-exx5-4xxc-9xxb-f2fxxxxxx41a
    &redirect_uri=https://www.example.com/

Next, pasting this link into your browser will direct you to a login page where you should input the credentials of the system user. After that a consent screen will come up like the screenshot below to prompt you to accept or reject the permission of the application to impersonate that user.

DocuSign authorization dialog box

Step 2: Create the JWT assertion (not needed for DocuSign SDKs)

To authenticate in the JWT Grant flow, you will need to create a JWT assertion containing data on the authentication request, then exchange it for an access token. If you’re using a DocuSign SDK, the SDK performs this step for you. 

A DocuSign JWT contains three JSON blocks that are encoded (https://jwt.io/ can verify the encoding for you) and separated by period characters: 

Header:

{
  "alg": "RS256",
  "typ": "JWT"
}

Payload:

{
  "iss": "<your_integration_key>",
  "sub": "<your_user_ID>",
  "aud": "account-d.docusign.com",
  "iat": <current_unix_epoch_time>,
  "exp": <current_unix_epoch_time+6000>,
  "scope": "signature impersonation"
}
  • "sub": the user ID of the user to be impersonated.
  • "iat": the start time of the validity of this JWT assertion.
  • "exp": the expiration time of the validity of this JWT assertion.
  • For Unix epoch time for the "iat" and the "exp" parameters, use https://www.epochconverter.com/.

Signature:

RSASHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(body),
  "<your_public_RSA_key>",
  "<your_private_RSA_key>"  
)

Step 3: Generating an access token

Using DocuSign’s SDKs:

No need to worry about the JWT assertion in step 2, as DocuSign SDKs handle that in the background.

Generating an access token using the SDKs is really simple as shown below.

C#

var clientId = "d30dc66f-xxxx-xxxx-xxxx-f2ff5c4c441a"; //Integration Key
var impersonatedUserId = "b7119312-xxxx-xxxx-xxxx-044f57b1654f";
var authServer = "account-d.docusign.com";

var docusignClient = new DocuSignClient();

OAuthToken accessTokenResult = docusignClient.RequestJWTUserToken(
    clientId, 
    impersonatedUserId,
    authServer, 
    File.ReadAllBytes("private.key"), // "private.key" file containing the RSA private key
    3600);

Console.WriteLine("JWT Access Token:");
Console.WriteLine(accessTokenResult.access_token);

Java

ApiClient apiClient = new ApiClient();
apiClient.setOAuthBasePath("account-d.docusign.com"); //Change to "account.docusign.com" for production

ArrayList<String> scopes = new ArrayList<>();
scopes.add("signature");
scopes.add("impersonation");

byte[] privateKeyBytes = Files.readAllBytes(Paths.get("private.key")); // "private.key" file containing the RSA private key

OAuthToken oAuthToken = apiClient.requestJWTUserToken(
    "c31826b6-xxxx-xxxx-xxxx-9bbd3b3fbf40", //Integration Key
    "e694aa11-xxxx-xxxx-xxxx-9c7e3de0e723", //Impersonated User Id
    scopes,
    privateKeyBytes,
    3600);
String accessToken = oAuthToken.getAccessToken();

System.out.println("JWT Access Token: " + accessToken);

Node.js

const docusign = require('docusign-esign')
    , fs = require('fs')
    , oauthServer = "account-d.docusign.com"
    , clientId = "c31826b6-xxxx-xxxx-xxxx-9bbd3b3fbf40" // Integration key
    , impersonatedUserId = "e694aa11-xxxx-xxxx-xxxx-9c7e3de0e723"
    , scopes = [ "signature", "impersonation"]
    , jwtLifeSec = 60 * 60 // requested lifetime for the JWT
    , dsApi = new docusign.ApiClient()
    , rsaKey = fs.readFileSync("private.key"); // "private.key" file containing the RSA private key

async function GetTokenJWT() {
    dsApi.setOAuthBasePath(oauthServer);
    return results = await dsApi.requestJWTUserToken(
        clientId,
        impersonatedUserId, 
        scopes, 
        rsaKey,
        jwtLifeSec
    );
}

 async function asyncMainline() {
    await GetTokenJWT();
    console.log(results.body.access_token);
    
}
asyncMainline()

PHP

<?php

require 'vendor/autoload.php';
require 'vendor/docusign/esign-client/autoload.php';
require 'vendor/docusign/esign-client/src/Client/ApiClient.php';
require 'vendor/docusign/esign-client/src/Configuration.php';
require 'vendor/docusign/esign-client/src/Client/Auth/OAuth.php';

$config = (new DocuSign\eSign\Configuration())->setHost("https://demo.docusign.net/restapi");
$oAuth = (new DocuSign\eSign\Client\Auth\OAuth())->setOAuthBasePath("account-d.docusign.com");

$apiClient = new DocuSign\eSign\Client\ApiClient($config, $oAuth);

$jwt_scope = array("signature", "impersonation");
$rsaPrivateKey = file_get_contents("private.key");

try {
    $response = $apiClient->requestJWTUserToken(
        "d30dc66f-xxxx-xxxx-xxxx-f2ff5c4c441a", // integration key
        "e694aa11-xxxx-xxxx-xxxx-9c7e3de0e723", // impersonated user id
        $rsaPrivateKey,
        $jwt_scope
    );
    
    echo "Access Token:" . "<br><br>";
    echo $response[0]['access_token'];
} catch (\Throwable $th) {
    echo $th;
}

Python

def _get_private_key():
    """
    Check that the private key present in the file and if it is, get it from the file.
    In the opposite way get it from config variable.
    """
    private_key_file = path.abspath(config["private_key_file"])

    if path.isfile(private_key_file):
        with open(private_key_file) as private_key_file:
            private_key = private_key_file.read()
    else:
        private_key = config["private_key_file"]

    return private_key

def _get_JWT_token(api_client):
    private_key = _get_private_key().encode("ascii").decode("utf-8")

    jwtToken = api_client.request_jwt_user_token(
        client_id = config["ds_client_id"],
        user_id = config["ds_impersonated_user_id"],
        oauth_host_name = config["authorization_server"],
        private_key_bytes = private_key,
        expires_in = 3600,
        scopes = ["signature", "impersonation"]
    )

    return jwtToken.access_token

Ruby

def authenticate
  configuration = DocuSign_eSign::Configuration.new
  configuration.debugging = true
  api_client = DocuSign_eSign::ApiClient.new(configuration)
  api_client.set_oauth_base_path(CONFIG['authorization_server'])

  rsa_pk = 'docusign_private_key.txt'
  begin
    token = api_client.request_jwt_user_token(CONFIG['jwt_integration_key'], CONFIG['impersonated_user_guid'], rsa_pk, 3600, $SCOPES)
  rescue OpenSSL::PKey::RSAError => e
    Rails.logger.error e.inspect

    raise "Please add your private RSA key to: #{rsa_pk}" if File.read(rsa_pk).starts_with? '{RSA_PRIVATE_KEY}'

    raise
  rescue DocuSign_eSign::ApiError => e
    body = JSON.parse(e.response_body)
      puts 'API Error'
      puts body['error']
      puts body['message']
      exit
    end
  end
end

Using Postman:

Method: POST

URL (demo): https://account-d.docusign.com/oauth/token

URL (production): https://account.docusign.com/oauth/token 

Params:

Key Value
grant_type urn:ietf:params:oauth:grant-type:jwt-bearer
assertion The JWT assertion created in step 2

Step 4: Using the access token

The access token generated in step 3 is valid for one hour, and during this hour it can be used to call DocuSign APIs by adding it as a bearer token in the authorization header.

Additional resources

Ahmed Shorim
Author
Ahmed Shorim
Sr. Developer Support Advisory Engineer
Published