Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Documentation] Amplifying refreshFn usage #1357

Open
punteroo opened this issue Jul 25, 2023 · 1 comment
Open

[Documentation] Amplifying refreshFn usage #1357

punteroo opened this issue Jul 25, 2023 · 1 comment
Labels
docs Documentation

Comments

@punteroo
Copy link

punteroo commented Jul 25, 2023

When creating a new connection on JSForce, a parameter can be passed along to the connection options that reflects logic that refreshes a connection's access token when expiring.

In the case of using a JWT server-to-server OAuth flow, the existence of a refresh token is nullified as it cannot be used, thus it cannot be passed on to the connection instance for auto-refreshing from the library itself. Therefore it is obligatory to refresh the access token every now and then via the same flow.

There is little to none documentation about how the refreshFn function works, I've investigated on how the inner works for this function are inside the library so I thought I'd give a little more insight from my behalf on how I solved this for our organization facing this issue where our session would die out after the access token expired, causing a bad implementation of this function to cause issues similar #356.

JWT OAuth Flow

Salesforce allows the usage of signed JWTs for authorizing users into an organization. In contrast of actual OAuth flows for users (such as username:password flows or clientId:clientSecret) this flow makes use of x509 certificate signed tokens to authorize access, while keeping scope permissions on the app's side. Documentation for this can be found here.

An integration for this type of authorization would be as follows (Typescript):

import { sign } from 'jsonwebtoken';

const payload = {
  iss: '3MVGxxxxxxxxxxxxxxxxx',
  aud: 'https://login.salesforce.com/',
  sub: '[email protected]',
  // 1 month expiry
  exp: Math.floor(Date.now() / 1000) * 30 * 24 * 60 * 60,
};

const token = sign(
  payload,
  // Decode the private key from base64.
  Buffer.from('privateKeyInBase64', 'base64').toString('utf-8'),
  { algorithm: 'RS256' },
);

const { access_token, instance_url } = await authenticateWithJwt(
  token,
  domain,
);

With a signed JWT following Salesforce's official documentation on the flow, one can send this token for Salesforce to authorize the request and return an access token.
This is a manual implementation for this call:

/**
 * Performs an OAuth 2.0 flow with Salesforce to authenticate the application using JWT.
 *
 * @param {string} token The signed JWT token to authenticate with.
 * @param {string} domain The domain to use (production or sandbox URLs)
 *
 * @return {Promise<SalesforceAuthResponse>} The response from the OAuth token API.
 */
async authenticateWithJwt(
  token: string,
  domain: string,
): Promise<SalesforceAuthResponse> {
  try {
    const { data } = await axios<SalesforceAuthResponse>({
      method: 'POST',
      url: `${domain}/services/oauth2/token`,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      data: stringify({
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        assertion: token,
      }),
    });

    return data;
  } catch (e) {
    this.logger.error(
      `Failed to authenticate with JWT! ${
        e?.response?.data ? JSON.stringify(e.response.data) : e
      }`,
    );

    throw e;
  }
}

domain is either the generic production or sandbox environment on Salesforce (https://login.salesforce.com for production or https://test.salesforce.com for sandbox). After a JWT is sent to Salesforce and authorized, a response from the API should be as seen in their documentation. If need be, access to JWT OAuth flow is detailed on top of the issue.

JSForce Implementation

Once the access_token and instance_url is obtained from Salesforce, assigning it to the connection is as basic as passing it as a parameter as follows:

import { Connection } from 'jsforce';

const connection = new Connection({
  instanceUrl: instance_url,
  accessToken: access_token,
});

This is fine for a short period of calls that need to be done on Salesforce, say a request scoped class is consuming this then it would authorize itself on each call. However, there may be use cases in which one would require for the connection to persist for as long the application is running, in our case for updates and service inter-connection.

The refreshFn Function

In this case, to refresh the session JSForce has a function parameter called refreshFn that exposes this functionality and allows one to override logic for token refreshing. It's functionality is hidden within the actual workings of the library, hence I wish to use this issue as an opportunity to amplify how it works and how to use it.

Within the Connection declarations, one can find how this parameter operates.

jsforce/lib/connection.js

Lines 155 to 163 in e011fc5

// Allow to delegate connection refresh to outer function
var self = this;
var refreshFn = options.refreshFn;
if (!refreshFn && this.oauth2.clientId) {
refreshFn = oauthRefreshFn;
}
if (refreshFn) {
this._refreshDelegate = new HttpApi.SessionRefreshDelegate(this, refreshFn);
}

It checks for the existance of a custom function being passed on and declares it for internal usage to override it. If it is present, it is passed on to the custom wrapper for HTTP calls onto Salesforce for refreshing when needed. This is what the SessionRefreshDelegate method does:

jsforce/lib/http-api.js

Lines 261 to 293 in e011fc5

var SessionRefreshDelegate = function(conn, refreshFn) {
this._conn = conn;
this._refreshFn = refreshFn;
this._refreshing = false;
};
inherits(SessionRefreshDelegate, events.EventEmitter);
/**
* Refresh access token
* @private
*/
SessionRefreshDelegate.prototype.refresh = function(since, callback) {
// Callback immediately When refreshed after designated time
if (this._lastRefreshedAt > since) { return callback(); }
var self = this;
var conn = this._conn;
var logger = conn._logger;
self.once('resume', callback);
if (self._refreshing) { return; }
logger.debug("<refresh token>");
self._refreshing = true;
return self._refreshFn(conn, function(err, accessToken, res) {
if (!err) {
logger.debug("Connection refresh completed.");
conn.accessToken = accessToken;
conn.emit("refresh", accessToken, res);
}
self._lastRefreshedAt = Date.now();
self._refreshing = false;
self.emit('resume', err);
});
};

It delegates token refreshing logic to whatever is defined inside the passed on function by reference, and interprets the result based on what the callback gives back onto its call. To put this into perspective, one would require to define asynchronous logic to ask for new tokens and resolve the callback with the resultant access token and emit the successful refresh.
Once a refresh event is emmited, the connection is stored again with its new parameters onto the same instance, thus providing an accesible refreshed connection from all consuming parts.

jsforce/lib/cli/cli.js

Lines 143 to 146 in e011fc5

conn.on('refresh', function(accessToken) {
print('Refreshing access token ... ');
saveCurrentConnection();
});

jsforce/lib/cli/cli.js

Lines 82 to 97 in e011fc5

function saveCurrentConnection() {
if (conn && connName) {
var connConfig = {
oauth2: conn.oauth2 && {
clientId: conn.oauth2.clientId,
clientSecret: conn.oauth2.clientSecret,
redirectUri: conn.oauth2.redirectUri,
loginUrl: conn.oauth2.loginUrl
},
accessToken: conn.accessToken,
instanceUrl: conn.instanceUrl,
refreshToken: conn.refreshToken
};
registry.saveConnectionConfig(connName, connConfig);
}
}

An Implementation

To implement a persistent refreshing using this logic, an example implementation would be as follows:

  1. A defining function that follows the specification for the refresh function
  2. Refreshing logic to abide by Salesforce's required JWT OAuth flow
  3. Resolution on callback for the Connection instance to update

Following the signing process described above, the refreshing function would look like this:

const refreshFn = (
  connection: Connection,
  callback: (err: Error, accessToken: string, res?: any) => void,
): any => {
  const refresh = async () => {
    // Ask for the access token
    try {
      // Re-sign a JWT token.
      const payload = {
        iss: clientId,
        aud: domain,
        sub: userName,
        // Expire in 6 months.
        exp: Math.floor(Date.now() / 1000) + 6 * 30 * 24 * 60 * 60,
      };

      const token = sign(
        payload,
        // Decode the private key from base64.
        Buffer.from(config.privateKey, 'base64').toString('utf-8'),
        { algorithm: 'RS256' },
      );

      // Fetch a new token.
      const { access_token } = await this.authenticateWithJwt(
        token,
        domain,
      );

      // Send this back to the connection instance.
      callback(null, access_token, access_token);
    } catch (e) {
      throw e;
    }
  };

  refresh();
};

Thus having the final Connection declaration passed on to any instance that requires it would be:

const connection = new Connection({
  instanceUrl: instance_url,
  accessToken: access_token,
  refreshFn,
});
@cristiand391 cristiand391 added the docs Documentation label Feb 16, 2024
@cristiand391
Copy link
Member

related: #1213

We'll keep both issues opened to make sure details are covered in the documentation update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Documentation
Projects
None yet
Development

No branches or pull requests

2 participants