Using a Refresh Token to obtain an Access Token from Docusign

Sanjucta Ghose
6 min readMar 3, 2019

--

This article explains how to obtain and use an Access Token from a Refresh Token in the Authorization Code Grant Flow for the Docusign REST API. I assume that the reader is aware of what Docusign is and what it is used for. The relevant developer documentation can be found here.

I was recently working on a project to create a small web application that could be used to send envelopes based on Docusign templates. A typical use case would be something like this — The user would fill in a form containing names and emails of recipients corresponding to template roles . On submitting the form an envelope would be created and sent to the recipients.

The web application is written in NodeJs using the Express framework. For authentication, I used Passport.js and the docusign-passport-strategy. To integrate with Docusign I used the NodeJs client (https://www.npmjs.com/package/docusign-esign). Code examples for using the docusign-esign library can be found at the repository (https://github.com/docusign/eg-03-node-auth-code-grant). These examples explain how to use the Authorization Code Grant Flow to obtain an access token that is required to make calls to the Docusign API. Whenever the user tries to peform any operation that requires a call to the Docusign Api, the application checks for a valid Access Token in the users session. If a valid access token is available, that is used to make the API call. If no access token in present in the session or the access token has expired( or is close to expiring) the user is asked to authenticate again.

What I needed, however, was to have the user login once and then have the application use the Refresh Token returned on the first successful authentication attempt to obtain a new Access Token so that the user would not need to login again and again. I could not find any concrete example code to do this and had to figure that out

To demonstrate what I learnt, I created a small project that can be found here (https://github.com/sanjucta/docusign-auth-code-grant).

Lets look at the important parts of the Auth Code Grant Flow. Firstly we need to configure the app to use express-session and passport. The handler for the /ds/login route calls the passport.authenticate method which redirects the user to the Docusign consent screen (hosted by Docusign). Once the user gives consent the user is redirected to the /ds/callback route with the auth code.

let app = express()
.use(cookieParser())
.use(session({ //Configure the app to use express-session
secret: dsConfig.sessionSecret,
name: 'ds-authexample-session',
cookie: {maxAge: max_session_min * 60000},
saveUninitialized: true,
resave: true,
store: new MemoryStore({
checkPeriod: 86400000 // prune expired entries every 24h
})}))
.use(passport.initialize()) //Configure the app to use passport .use(passport.session()).use(bodyParser.urlencoded({ extended: true })).get('/ds/login', (req, res, next) => {passport.authenticate('docusign')(req, res, next);}).get('/ds/callback', [dsLoginCB1, dsLoginCB2]); // OAuth callbacks.

The dsLoginCB1 callback method calls the passport.authenticate() method to exchange the auth code for an access token.

function dsLoginCB1 (req, res, next) {passport.authenticate('docusign', { failureRedirect: '/ds/login' })(req, res, next);}

Passport is configured to use the DocusignStrategy for authentication. After successful authentication and after the auth code has been exchanged for an access token passport calls the _processDsResult callback with the access token, refresh token and user profile.

// Configure passport for DocusignStrategylet docusignStrategy = new DocusignStrategy({production: dsConfig.production,clientID: dsConfig.dsClientId,clientSecret: dsConfig.dsClientSecret,callbackURL: hostUrl + '/ds/callback',state: true // automatic CSRF protection.},function _processDsResult(accessToken, refreshToken, params, profile, done) {let user = profile;user.accessToken = accessToken;user.refreshToken = refreshToken;user.expiresIn = params.expires_in;user.tokenExpirationTimestamp = moment().add(user.expiresIn, 's'); // The dateTime when the access token will expire//Save the encrypted refresh token to be used to get a new access token when the current one expiresnew Encrypt(dsConfig.refreshTokenFile).encrypt(refreshToken);return done(null, user);});

Note the call to the done() callback above which is passed the user object. From within this callback, passport calls the method specified to serialize the user object into the session.

// Passport session setup.// To support persistent login sessions, Passport needs to be able // to serialize users into and deserialize users out of the session.    // Typically,this will be as simple as storing the user ID when  
// serializing, and finding the user by ID when deserializing.
// However, since this example does not have a database of user
// records, the complete DocuSign profile is serialized
// and deserialized.
passport.serializeUser (function(user, done) {console.log("In serialize user");done(null, user)});passport.deserializeUser(function(obj, done) {console.log("In de-serialize user");done(null, obj);});

So we can see that most of the heavy lifting is done by passport and the passport docusign strategy including redirecting the user to the page where she can enter her credentials, sending back the auth code to retrieve the access token, obtaining the user profile along with the access and refresh tokens and serializing the user in to the session with the help of the call back function specified.

How does the application check for the presence of a valid access token. The code for that looks like this :-

function hasToken(req,bufferMin = tokenReplaceMinGet)
{
let noToken = !req.user || !req.user.accessToken ||
!req.user.tokenExpirationTimestamp
, now = moment()
, needToken = noToken
|| moment(req.user.tokenExpirationTimestamp).subtract(bufferMin,
'm').isBefore(now);

if (noToken) {console.log('hasToken: Starting up--need a token')}
if (needToken && !noToken)
{console.log('checkToken: Replacing old token')}
if (!needToken) {
console.log('checkToken: Using current token')
}
return (!needToken)
}

The application checks for the presence of the access token in the req.user object and looks at the token expiration timestamp to determine if the token has expired or will expire soon. In case it determines that a new access token is needed, it has two options

  1. Redirect the user to the authentication flow again
  2. Exchange the refresh token that it received when the user previously authenticated for a new access token.

Passport or the passport docusign strategy do not help in the second case. For that we need to fall back upon the docusign nodeJS sdk. But before we can exchange the refresh token for a new access token , we need to save the refresh token that we received when the user authenticated. The refresh token can be saved into a database but for the purposes of this example, we encrypt the refresh token and save it in a file. For that we write a simple utility class whose encrypt method encrypts the password save saves it to a file.

encrypt(data) {        
try {
var cipher = Crypto.createCipher('aes-256-cbc', this.password);
var encrypted = Buffer.concat([cipher.update(new
Buffer(JSON.stringify(data), "utf8")), cipher.final()]);
FileSystem.writeFileSync(this.filePath, encrypted);
return { message: "Encrypted!" };
}catch(exception) {
throw new Error(exception.message);
}
}
//In callback( _processDsResult) invoked on successful authentication call this method to save the encypted refresh token. new Encrypt(dsConfig.refreshTokenFile).encrypt(refreshToken);

Now, when the the application determines that a new access token is needed it checks if a file containing the encrypted refresh token exists and if so it calls the getAccessTokenUsingRefreshToken() method. This method reads the encrypted refresh token and posts a request to the Docusign Rest API ‘/auth/token’ endpoint.

const refreshToken = new    
Encrypt(dsConfig.refreshTokenFile).decrypt();
const clientString = clientId + ":" + clientSecret,
postData = {
"grant_type": "refresh_token",
"refresh_token": refreshToken, },
headers= {
"Authorization": "Basic " + (new
Buffer(clientString).toString('base64')),
},
authReq = superagent.post( dsConfig.dsOauthServer +
"/oauth/token")
.send(postData)
.set(headers)
.type("application/x-www-form-urlencoded");

Our work does not end after a new access token is received. Recall that on successful authentication passport also returned a user profile to us. In this case we need to retrieve the user profile ourselves. For that we use the the userProfile method that the DocusignSrategy provides us.

//Obtain the user profile          docusignStrategy.userProfile(accessToken, function(err,profile)                {              
if (err)
{
console.log("ERROR getting user profile:");
console.log(err);
return callback(err, authRes);
}else{
let user = profile;
user.accessToken = accessToken;
user.refreshToken = refreshToken;
user.expiresIn = expiresIn;
user.tokenExpirationTimestamp = moment().add(user.expiresIn,
's'); // The dateTime when the access token will expire
}

Once we have the user profile object we need to serialize the user object into the session.This is automatically taken care of by passport when the user authenticates successfully and the ‘done’ callback is invoked by the ‘_processDsResult’ callback. But since passport is not involved when getting an access token from a refresh token, we need to find some other way to do this. Passport exposes a login method on the req object which does what we are looking for. So now our code looks like this.

docusignStrategy.userProfile(accessToken, function(err,profile)                {              
if (err)
{
console.log("ERROR getting user profile:");
console.log(err);
return callback(err, authRes);
}else{
let user = profile;
user.accessToken = accessToken;
user.refreshToken = refreshToken;
user.expiresIn = expiresIn;
user.tokenExpirationTimestamp = moment().add(user.expiresIn,
's'); // The dateTime when the access token will expire
}
req.login(user,(err)=>{
// Most Docusign api calls require an account id.
//This is where you can fetch the default account id for the
//user and store in the session.
callback();
}
)}

Once the req.login method is called the user object is serialized into the session and out hasToken() method returns true and the application can read the access token from the req.user object.

--

--