├── .gitignore ├── document.pdf ├── private.key ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | private.key 3 | bulkImports.txt 4 | -------------------------------------------------------------------------------- /document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docusign/AccessTokenDemo/main/document.pdf -------------------------------------------------------------------------------- /private.key: -------------------------------------------------------------------------------- 1 | {copy and paste your private key here, include the beginning and end strings too} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodedemo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "docusign-admin": "^1.0.2", 8 | "docusign-click": "^1.1.0", 9 | "docusign-esign": "^5.16.0", 10 | "open": "^8.4.0" 11 | }, 12 | "devDependencies": {}, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "", 17 | "license": "ISC" 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AccessTokenDemo 2 | DocuSign DevCon Conference: Access Token Generator Demo 3 | 4 | -- Don't be scared, give it a whirl! -- 5 | 6 | To run this demo: 7 | 8 | 1. Open this repository locally and run `npm install`. 9 | 2. Login to your DocuSign developer account located at developers.docusign.com. Visit `My Apps and Keys` and save the user id at Impersonation Guid in `index.js`. 10 | 2. Create a new integration for your DocuSign Developer account on your apps and keys page. Save the Integration key in `index.js`. 11 | 3. Edit this new integration and create a new RSA keypair. Save the private key as a file named private.key in the same directory as `index.js`. 12 | 4. Run it! `node index.js` -> click the link to login (a first time) and grant application consent* 13 | 5. Run it again! `node index.js` to see a generated access token, user info, and (if configured) an organization ID. 14 | 15 | 16 | \* Before you can make any API calls using JWT Grant, you must get your user’s consent for your app to impersonate them. 17 | 18 | # Under the hood 19 | 20 | This is a simple node.JS script that harnesses the DocuSign eSignature and Admin SDKs to complete the OAuth portion of a DocuSign integration. DocuSign SDKs harness promises in Node which means you'll need to resolve callbacks using promise chains ( like .then({}).catch({}).finally({}) ) OR using Async/Await functions. 21 | 22 | For the sake of simplicity I've gone about it using an [Immediately invoking function expression](https://developer.mozilla.org/en-US/docs/Glossary/IIFE), due to superior readability. As a bonus, I kept a small portion of the old code commented for you to glean ideas from. 23 | 24 | Finally, in an attempt to make this code resuable for others, I've created a parent `DS` function that binds all methods into their own child functions under this parent DS object, (as in DS.getUserInfo() or DS.getJWT()). This will allow you modularize the code to use in other scripts (like `export default DS;`, `import DS from {DS};` ) 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // call in the DocuSign eSignature API 2 | let docusign = require("docusign-esign"); 3 | // call in the DocuSign Admin API 4 | let adminApi = require("docusign-admin"); 5 | // call in the DocuSign Click API 6 | let clickApi = require("docusign-click"); 7 | 8 | // call in fs (filesystem) module to point to a file on our hard disk 9 | let fs = require("fs/promises"); 10 | 11 | // call in open module to use filesystem to open URLs 12 | let open = require("open"); 13 | 14 | // call in the process.exit() function to stop the script for error handling 15 | const { exit } = require("process"); 16 | 17 | // Fill in an email address here to sign a demo document 18 | let emailAddy = ""; 19 | 20 | // These are the variables we intend to set using today's javascript demo. 21 | var accessToken, expiry, accountId, refreshToken, organizationId, clickwraps, envelopes; 22 | 23 | // These are on your apps and keys page at https://developers.docusign.com 24 | let impersonationUserGuid = ""; 25 | let integrationKey = ""; 26 | 27 | // For Authorization Code Grant ONLY 28 | let secretKey = ""; 29 | 30 | // 'signature' for eSignature, organization_read to retreive your OrgId 31 | let scopes = "signature+organization_read+click.manage+user_write"; 32 | 33 | // This is also set for for specific Integration key, found on the Apps and Keys page 34 | 35 | let redirectUri = "https://httpbin.org/get"; 36 | 37 | // NOTE: change this to account.docusign.com for production 38 | let oAuthBasePath = "account-d.docusign.com" 39 | 40 | // NOTE: change this to https://docusign.net/ for production 41 | let ApiBasePath = "https://demo.docusign.net"; 42 | 43 | 44 | // The a bit of a gotcha going on here. When providing scopes for generating your access token, specifing 'impersonation' is not necessary 45 | // after the first time because you're using the mechanism itself, IMPLYING, that jwt is being used. HOWEVER, the very first time we confirm 46 | // grant consent, we DO need to include the impersonation scope to grant JWT future consent. If you do not include impersonation, the JWT 47 | // request user token method will continue to return 'consent_required'. 48 | let consentUrl = `https://${oAuthBasePath}/oauth/auth?response_type=code&scope=impersonation+${scopes}&client_id=${integrationKey}&redirect_uri=${redirectUri}`; 49 | 50 | 51 | 52 | // Setting a global DocuSign (DS) object so we can reuse the function elsewhere. 53 | let DS = {}; 54 | 55 | // Sets the accessToken and expiry variables 56 | DS.getJWT = async function _getJWT() { 57 | try { 58 | let apiClient = new docusign.ApiClient(); 59 | apiClient.setOAuthBasePath(oAuthBasePath); 60 | 61 | let privateKey = await fs.readFile("private.key", "utf8"); 62 | 63 | // Let's get an access token 64 | let response = await apiClient.requestJWTUserToken(integrationKey, impersonationUserGuid, scopes, privateKey, 3600); 65 | 66 | // Show the API response 67 | console.log(response.body); 68 | 69 | // Save the expiration time and accessToken variables 70 | expiry = response.body.expires_in; 71 | accessToken = response.body.access_token; 72 | 73 | // Accessible JSON from module exports 74 | return { "expiry": expiry, "accessToken": accessToken }; 75 | 76 | } catch (err) { 77 | // Let's check if there's even a response body before trying to parse it 78 | if (err.response) { 79 | // The time spent to find this line that took me more effort than I'd like to admit 80 | 81 | if (err.response.body.error == "consent_required") { 82 | console.log("Consent required"); 83 | // Interesting quirk - any user can grant consent for 84 | // their user GUID through your integration key's URL 85 | console.log("Consent URL: " + consentUrl); 86 | await open(consentUrl, { wait: true }); 87 | // Exit since we cannot run further API calls 88 | exit(0); 89 | } 90 | } else { 91 | 92 | // Something else has gone wrong, let's halt execution further 93 | console.error(err); 94 | exit(1); 95 | } 96 | } 97 | } 98 | 99 | // Sets the Account Id variable 100 | DS.getUserInfo = async function _getUserInfo(accessToken) { 101 | 102 | let apiClient = new docusign.ApiClient(); 103 | apiClient.setOAuthBasePath(oAuthBasePath); 104 | 105 | // Let's get the API Account ID 106 | let response = await apiClient.getUserInfo(accessToken); 107 | 108 | // Show the API Response 109 | console.log(response); 110 | 111 | // Save the API Account ID to a variable 112 | accountId = response.accounts[0].accountId 113 | 114 | // Accessible JSON from module exports 115 | return new Promise(async resolve => { 116 | resolve({ "accountId": accountId }) 117 | 118 | }); 119 | } 120 | 121 | 122 | // Sets the accessToken, expiry, and refresh token variables 123 | DS.getAuthCodeGrantToken = async function _getAuthCodeGrantToken() { 124 | return new Promise(async (resolve, reject) => { 125 | // start webserver 126 | console.log("Listening on Port 5000"); 127 | const http = require('http'); 128 | http.createServer(async function (req, res) { 129 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 130 | res.write('Received Authorization Code, You may close this window now'); 131 | res.end(); 132 | 133 | if (req.url.includes("code=")) { 134 | let rawResult = req.url.toString(); 135 | let authorizationCode = rawResult.replace("/?code=", ""); 136 | console.log("Authorization Code is:", authorizationCode); 137 | 138 | 139 | try { 140 | let apiClient = new docusign.ApiClient(); 141 | apiClient.setOAuthBasePath(oAuthBasePath); 142 | let response = await apiClient.generateAccessToken(integrationKey, secretKey, authorizationCode); 143 | // Show the API response 144 | console.log(response); 145 | 146 | // Save the expiration time, accessToken, and refreshToken variables 147 | expiry = response.expiresIn; 148 | 149 | // A token is a token is a token! This Access token will work just the same will other API calls below 150 | accessToken = response.accessToken; 151 | 152 | // Access tokens provided by Authorization Code Grant will last for 8 hours. 153 | // Use this refresh token to allow them to generate a new one without needing 154 | // to login again. The refresh token is valid for 30 days. 155 | refreshToken = response.refreshToken; 156 | 157 | // Accessible JSON from module exports 158 | return resolve({ "expiry": expiry, "accessToken": accessToken, "refreshToken": refreshToken }); 159 | } 160 | catch (err) { 161 | console.log(err); 162 | return reject(err); 163 | } 164 | } 165 | 166 | }).listen(5000); 167 | // Use the consent URL to login 168 | await open(`https://${oAuthBasePath}/oauth/auth?response_type=code&scope=${scopes}&client_id=${integrationKey}&redirect_uri=http://localhost:5000`, { wait: true }) 169 | 170 | 171 | }); 172 | }; 173 | 174 | // Sets the accessToken, expiry, and refresh token variables 175 | const axios = require('axios'); 176 | 177 | DS.refreshAccessToken = async function _refreshAccessToken(clientId, clientSecret, refreshToken) { 178 | const tokenEndpoint = 'https://account-d.docusign.com/oauth/token'; 179 | 180 | // Prepare the request body and header 181 | const requestBody = new URLSearchParams(); 182 | requestBody.append('grant_type', 'refresh_token'); 183 | requestBody.append('refresh_token', refreshToken); 184 | 185 | const authHeaderValue = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); 186 | 187 | const config = { 188 | headers: { 189 | 'Content-Type': 'application/x-www-form-urlencoded', 190 | 'Authorization': `Basic ${authHeaderValue}`, 191 | }, 192 | }; 193 | 194 | try { 195 | const response = await axios.post(tokenEndpoint, requestBody, config); 196 | const { access_token, refresh_token, expires_in } = response.data; 197 | 198 | console.log('New access token:', access_token); 199 | console.log('New refresh token:', refresh_token); 200 | console.log('Expires in:', expires_in); 201 | 202 | // Save or return the new access token, refresh token, and expires_in as needed 203 | return { access_token, refresh_token, expires_in }; 204 | } catch (error) { 205 | console.error('Error refreshing access token:', error.response.data); 206 | throw error; 207 | } 208 | } 209 | 210 | 211 | // Sets the Organziation ID variable 212 | DS.getOrgId = async function _getOrgId(accessToken) { 213 | try { 214 | 215 | let adminClient = new adminApi.ApiClient(); 216 | // The Admin API uses a Different base path 217 | adminClient.setBasePath("https://api-d.docusign.net/management"); 218 | adminClient.addDefaultHeader('Authorization', `Bearer ${accessToken}`); 219 | 220 | // Instantiate the DocuSign Admin's Accounts API 221 | let accounts = new adminApi.AccountsApi(adminClient); 222 | 223 | // Let's get the Organization Id using the Admin API 224 | let response = await accounts.getOrganizations(); 225 | 226 | // Show the API Response 227 | console.log(response); 228 | 229 | // Save the Organization ID to a variable IF we belong to an organization 230 | if (response.organizations.length > 0) { 231 | organizationId = response.organizations[0].id; 232 | 233 | // Accessible JSON from module exports 234 | return { "organizationId": organizationId }; 235 | } 236 | else { 237 | console.log("User does not belong to an organization"); 238 | } 239 | 240 | 241 | 242 | } catch (err) { 243 | console.error(err); 244 | }; 245 | }; 246 | 247 | DS.deleteBulkImportIds = async function _deleteBulkImportIds(accessToken, organizationId) { 248 | 249 | try { 250 | 251 | let adminClient = new adminApi.ApiClient(); 252 | // The Admin API uses a Different base path 253 | adminClient.setBasePath("https://api-d.docusign.net/management"); 254 | adminClient.addDefaultHeader('Authorization', `Bearer ${accessToken}`); 255 | 256 | // Instantiate the DocuSign Admin's Accounts API 257 | let bulkImports = new adminApi.BulkImportsApi(adminClient); 258 | 259 | // This bulk imports file is just a text file with an import guid on each line, no quotes 260 | let textFile = await fs.readFile('bulkImports.txt', "utf-8"); 261 | const lines = textFile.split(/\r?\n/); 262 | 263 | lines.forEach(async (line) => { 264 | let response = await bulkImports.deleteBulkUserImport(organizationId, line, (response) => { 265 | console.log("deleting record for", line, response); 266 | }); 267 | 268 | }) 269 | 270 | 271 | 272 | } catch (err) { 273 | console.log(err) 274 | } 275 | 276 | }; 277 | 278 | DS.sendEnvelope = async function _sendEnvelope(accessToken, accountId, emailAddy) { 279 | try { 280 | let apiClient = new docusign.ApiClient(); 281 | apiClient.setBasePath(ApiBasePath + "/restapi"); 282 | apiClient.addDefaultHeader("Authorization", `Bearer ${accessToken}`); 283 | 284 | let envelopesApi = new docusign.EnvelopesApi(apiClient); 285 | 286 | let envelopeDefinition = new docusign.EnvelopeDefinition(); 287 | envelopeDefinition.emailSubject = "Please sign this document"; 288 | envelopeDefinition.status = "sent"; 289 | 290 | let doc = new docusign.Document(); 291 | doc.documentBase64 = await fs.readFile("document.pdf", { encoding: "base64" }); 292 | doc.name = "Sample Document"; 293 | doc.fileExtension = "pdf"; 294 | doc.documentId = "1"; 295 | 296 | envelopeDefinition.documents = [doc]; 297 | 298 | let signer = new docusign.Signer(); 299 | signer.email = emailAddy; 300 | signer.name = "John Doe"; 301 | signer.recipientId = "1"; 302 | signer.routingOrder = "1"; 303 | 304 | let signHere = new docusign.SignHere(); 305 | signHere.documentId = "1"; 306 | signHere.pageNumber = "1"; 307 | signHere.recipientId = "1"; 308 | signHere.tabLabel = "SignHereTab"; 309 | signHere.anchorString = "/sn1/"; 310 | signHere.anchorUnits = "pixels"; 311 | signHere.anchorXOffset = "20"; 312 | signHere.anchorYOffset = "10"; 313 | 314 | 315 | let tabs = new docusign.Tabs(); 316 | tabs.signHereTabs = [signHere]; 317 | signer.tabs = tabs; 318 | 319 | let recipients = new docusign.Recipients(); 320 | recipients.signers = [signer]; 321 | envelopeDefinition.recipients = recipients; 322 | 323 | let response = await envelopesApi.createEnvelope(accountId, { envelopeDefinition: envelopeDefinition }); 324 | 325 | // Show the API response 326 | console.log(response); 327 | 328 | return { "response": response }; 329 | } catch (err) { 330 | console.error(err); 331 | } 332 | }; 333 | 334 | 335 | DS.getEnvelopes = async function _getEnvelopes(accessToken, accountId) { 336 | try { 337 | let apiClient = new docusign.ApiClient(); 338 | apiClient.setBasePath(ApiBasePath + "/restapi"); 339 | apiClient.addDefaultHeader("Authorization", `Bearer ${accessToken}`); 340 | 341 | let envelopesApi = new docusign.EnvelopesApi(apiClient); 342 | const msSinceEpoch = (new Date()).getTime(); 343 | // 720 hours OR 30 days ago 344 | const thirtyDaysAgo = new Date(msSinceEpoch - 720 * 60 * 60 * 1000).toISOString(); 345 | let options = { fromDate: thirtyDaysAgo }; 346 | 347 | let response = await envelopesApi.listStatusChanges(accountId, options); 348 | 349 | // Show the API response 350 | console.log(response); 351 | 352 | envelopes = response.body; 353 | 354 | return envelopes; 355 | } catch (err) { 356 | console.error(err); 357 | } 358 | } 359 | 360 | DS.getClickwraps = async function _getClickwraps(accessToken, accountId) { 361 | try { 362 | let apiClient = new clickApi.ApiClient(); 363 | apiClient.setBasePath(ApiBasePath + "/clickapi"); 364 | apiClient.addDefaultHeader("Authorization", `Bearer ${accessToken}`); 365 | 366 | let accounts = new clickApi.AccountsApi(apiClient); 367 | 368 | let response = await accounts.getClickwraps(accountId); 369 | 370 | // Show the API response 371 | console.log(response); 372 | 373 | clickwraps = response.body 374 | 375 | return clickwraps; 376 | } catch (err) { 377 | console.error(err); 378 | } 379 | }; 380 | 381 | 382 | // go live populated transactions test 383 | DS.getAccount = function _getAccount(accessToken, accountId) { 384 | try { 385 | 386 | let apiClient = new docusign.ApiClient(); 387 | apiClient.setBasePath(ApiBasePath + "/restapi"); 388 | apiClient.addDefaultHeader("Authorization", `Bearer ${accessToken}`); 389 | 390 | let accounts = new docusign.AccountsApi(apiClient); 391 | let response = accounts.getAccountInformation(accountId); 392 | 393 | // Show the API response 394 | console.log(response); 395 | } catch (err) { 396 | console.error(err); 397 | }; 398 | }; 399 | 400 | // Main code execution - this will execute immediately after being read 401 | (async () => { 402 | 403 | //await DS.getJWT(); 404 | const authCodeGrantTokenResult = await DS.getAuthCodeGrantToken(); 405 | accessToken = authCodeGrantTokenResult.accessToken 406 | console.log("accessToken " + accessToken) 407 | 408 | exit(0); 409 | 410 | const userInfo = await DS.getUserInfo(accessToken); 411 | console.log("userInfo" + JSON.stringify(userInfo)); 412 | 413 | exit(0); 414 | 415 | const sentEnvelope = await DS.sendEnvelope(accessToken, userInfo.accountId, emailAddy); 416 | console.log("Sent Envelope: " + JSON.stringify(sentEnvelope)); 417 | exit(0); 418 | await DS.refreshAccessToken(integrationKey, secretKey, authCodeGrantTokenResult.refreshToken); 419 | await DS.getEnvelopes(accessToken, userInfo.accountId); 420 | await DS.getOrgId(accessToken); 421 | await DS.getClickwraps(accessToken, userInfo.accountId); 422 | 423 | })(); 424 | 425 | 426 | /* 427 | ****************************************** 428 | LONGHANDED CALLBACK oldschool sort-of way. 429 | IIFE (as above) is easier to read! 430 | 431 | JS is non blocking which means callbacks, promises, 432 | or async/await. I went async/await, but this 433 | is how it would work using promises: 434 | ****************************************** 435 | 436 | DS.getJWT().then((response) => { 437 | // Sanity Check to verify we're passing in our token 438 | console.log("Access Token" , response.body.access_token); 439 | accessToken = response.body.access_token 440 | 441 | DS.getUserInfo(accessToken).then((response) => { 442 | console.log("UserInfo Response", response) 443 | // Save the API Account Id 444 | accountId = response.accounts[0].exports.accountId 445 | }) 446 | }); 447 | 448 | */ --------------------------------------------------------------------------------