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

How to use in clustered node server (PM2)? #123

Open
ysageev opened this issue Jan 12, 2022 · 4 comments
Open

How to use in clustered node server (PM2)? #123

ysageev opened this issue Jan 12, 2022 · 4 comments

Comments

@ysageev
Copy link

ysageev commented Jan 12, 2022

I'm struggling trying to implement OAuth2 in a pm2 clustered environment. The example provided stores the new OAuthClient in a global variable. This is great if everyone is connecting to that one server instance. In production that is almost never the case.

I am going to assume that if process 1 and process 2 create wholly new OAuthClients, then access tokens returned to 2 will invalidate tokens in 1. Is that correct?

If so, I need a way to reconstitute an OAuthClient from data stored in a database. I imagine the flow will look like this:

  1. On connect request, check global variable is populated.
  2. If not, grab the access and refresh tokens and expiry from the database
  3. Recreate OAuthClient and manually and enter data from (2). HOW?
  4. If access token has expired, call RefreshToken() etc. ...
@lsacco-nutreense
Copy link

lsacco-nutreense commented Mar 24, 2022

I have the same issue. Have you figured out a solution?

We follow this process:

  1. Authenticate a QBO user
  2. Set-up a callback to our app to save the resulting access/refresh token
  3. On subsequent calls, check to see if the access token is valid; if not, refresh the token
  4. Save the new access/refresh token in our DB; repeat step 3 as needed.

This works great in a single-node environment, but when this runs in a clustered environment, we see this error:

{"authResponse":{"body":"{\"error\":\"invalid_grant\"}","intuit_tid":"1-623b94f9-1d22e7c022d9091a3c2281c8","json":{"error":"invalid_grant"}

This is the relevant code:

  private async validateToken() {
    // Need to double check if oauthClient is available when in multi-pod HA environment
    if (!this.oauthClient) {
      this.oauthClient = new OAuthClient({
        clientId: environment.INTUIT_CLIENT_ID,
        clientSecret: environment.INTUIT_CLIENT_SECRET,
        environment: environment.INTUIT_ENV,
        redirectUri: environment.INTUIT_REDIRECT_URI,
        logging: true
      });
    }
    const isTokenValid = await this.oauthClient.isAccessTokenValid();
    if (isTokenValid) {
      this.logger.log('Token is valid');
    } else {
      const testToken = await this.oauthClient.token.getToken();
      if (!testToken.refresh_token) {
        const dbdoc = await this.findToken(TOKEN_NAME);
        this.token = dbdoc.token;
        this.oauthClient.setToken(this.token);
        const isValid = await this.oauthClient.isAccessTokenValid();
        if (!isValid) {
          await this.refreshToken();
        }
      } else {
        await this.refreshToken();
      }
    }
  }

  private async refreshToken() {
    await this.oauthClient
      .refresh()
      .then((res) => {
        this.intuitTokenDto.token = res.token;
        this.createOrUpdateToken({ name: TOKEN_NAME }, this.intuitTokenDto);
      })
      .catch((err) => {
        const msg = 'Could not refresh access token. User must reauthenticate to Intuit for new token.';
        err.error = msg;
        throw err;
      });
  }

@ysageev
Copy link
Author

ysageev commented Mar 24, 2022

Right now I just use a single server instance. :-/

I don't anticipate it being that bad for now because the proportion of users actually hitting QBO is very small -- just those with accounting privileges.

Eventually I will revisit the clustered environment, and imagine that it will work by deeply serializing the oauthClient into a database or local json file, check if it is there, and reconstitute the oauthClient from that data if it is. I don't have the cycles for that at this time.

It would be nice if the API had toJSON() and fromJSON() methods that could attempt to instantiate the client in this way, or something similar. Currently, it really looks like the API is not designed to be used in a clustered environment.

@robreiss
Copy link

I treat the refresh_token as a single-use token. When a new access_token is obtained, I store the new access_token and refresh_token in a central database. In a cloud server environment where multiple instances of my program might attempt to access QuickBooks simultaneously, a race condition can occur. To address this, I serialize access to the stored refresh_token using Postgres.

I store the tokens in a table row designated for each company_id and include a column that serves as a lock when refreshing the tokens. This ensures that only one process can refresh the tokens at any given time. While I have not extensively tested this solution in a production setting, it appeared to work effectively during the limited testing conducted before the project shifted away from QuickBooks.

@antiquicksort
Copy link

@abisalehalliprasan Do you know a workaround for this?

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

No branches or pull requests

5 participants