Search code examples
node.jsoauth-2.0slackslack-apirefresh-token

Using a refresh token to get a bot access token replaces the refresh token


I am trying to convert my Slack app code to use bot refresh tokens rather than the old bot token when accessing the web client api.

I have been able to call the https://slack.com/api/oauth.v2.access endpoint both from Postman and using the Slack Webclient node.js module.

I am passing in my app's client id, client secret, my bot refresh token and a grant_type of refresh_token.

In both cases the Slack API responds with a new access_token but sometimes I also get a new refresh_token and I can't use my original refresh token to get more access tokens.

This doesn't seem correct;

I have stored the refresh token from the app OAuth web page in a secret store. Typically you would use the non-expiring refresh token to get access tokens as required.

Since Slack is also refreshing the refresh_token each time, I am unable to rely on the secret store and I will need to persist this refresh token in some other place, which is potentially insecure.

When the refresh token changes it also invalidates the installation object that I have persisted and I need to re-do the "Add to Slack" process to fix that.

Am I doing something wrong, such as using the wrong endpoint, or is the Slack API "broken"?

Update & clarification

Thanks for those in the comments who have pointed out that issuing a new refresh token is in the OAuth spec.

The reason that this is a problem for me is that I need bot access tokens in two places. In one of those places the token refresh is handled by Slack's Bolt framework. In the other I am handling token refresh myself.

Since a new refresh token is issued when the access token is refreshed, one of those two places no longer knows the "current" refresh token, so it fails next time it tries to refresh its access token. Which one fails depends on which one refreshed the token.

The comment on the refresh token in the Slack OAuth page for the app says "never expires", but this is misleading as while it doesn't expire per-se, the value shown in the page does become invalid when a new access token is generated from it.

This is quite different behaviour from other "never expires" tokens such as the app client secret, which is only invalidated if you manually re-issue it in the settings page.

Prior to token rotation, slack apps used a non-expiring "Bot token" that could be used in multiple places. It seems that you cannot get the exact behaviour with OAuth and token rotation.


Solution

  • As @Codebling and @morganney pointed out, it is within the OAuth spec for a new refresh token to be issued alongside a new access token.

    My problem arises because I need an access token in two separate parts of my app. One part is using Slack's Bolt framework and the other isn't. This leads to a potential for conflict where the refresh token is updated by one or the other parts of my app.

    The solution is to leverage the Slack Bolt InstallProvider in the non-Bolt code in my app (This requires that the non-Bolt code can access the same InstallationStore implementation as the Bolt code).

    Using the InstallProvider ensures that if a token refresh is required, the InstallationStore is always updated and a consistent set of tokens (refresh and access) is available to both parts of my app.

    In the non-Bolt code I use the following to obtain an access token to call the Slack Web client:

    const installer = new InstallProvider({
              clientId: process.env.SLACK_CLIENT_ID!,
              clientSecret: process.env.SLACK_CLIENT_SECRET!,
              installationStore: new MyInstallationStore(),
              stateSecret: 'someSecret'
            })
    
    const authorizeResult = await installer.authorize({
              isEnterpriseInstall: enterpriseId !== undefined,
              enterpriseId: enterpriseId,
              teamId: teamId
    })
    
    if (authorizeResult.botToken) {
        await this.webclient.chat.postMessage({
                 channel: slackId,
                 blocks: blocks,
                 icon_emoji: `:wave:`,
                 text: "Welcome message",
                 token: authorizeResult.botToken,
        });
    }