slackapi / node-slack-sdk

Slack Developer Kit for Node.js
https://slack.dev/node-slack-sdk
MIT License
3.27k stars 661 forks source link

Why do I get invalid_refresh_token error when trying to refresh the token for Slack? #2038

Open parth-007 opened 1 week ago

parth-007 commented 1 week ago

I am developing something like Zapier/N8N, in which user can come and create Slack integration and then installs the app in his workspace. Tokens are generated and I stored them in the DB.

Earlier I was using long-lived bot and user tokens. I wanted to have the tokens refreshed. I am following this: https://api.slack.com/authentication/rotation

Note: I have different bot/user scopes in my App home page and I request different scopes from my code when generating OAuth URL (Which is prompted to the User. Is this okay?

Below Screen:

O9Vhyh61

I added the code to generate refresh token, access token, expires_in. Now suppose, User comes and he creates a new integration, fresh set of access_token and refresh_token along with expiry are generated. Now in my UI, if user performs some action like Create channel/send msg to user. I check by comparing the current time and the expires_in, if the current time is more than I perform refresh using oauth.v2.access.

Is it correct that for every user, I have unique set of access and refresh tokens? Which are different from the ones listed in the Slack App home page

How did I test this on my local that it works?

I manually updated my expires_in to be 1 day back, so automatically refresh was done.

Now on my server(Deployed. code),

I created an integration(access_token, refresh_token were generated) and did not perform any action for 12 hours. Then I tried to do some Slack action, I got below error:

{
    "ok": false,
    "error": "invalid_refresh_token"
}

I read the same from here also:

https://forums.slackcommunity.com/s/question/0D5Hq00009OgLQgKAN/what-is-the-expiration-time-for-refresh-token-?language=en_US

So is it correct behavior that refresh_token is also expired in 12 hours so it is mandatory for the user to ask for refresh in 12 hour limit from where the access token was generated. Any attempt to refresh it after the 12 hours period with the refresh token will result in error?

I also read that a schedule task should be created. But imagine I have so many of slack integrations from users so I can't have a watch on them and update them when the expiry time is near.

Is my understanding correct? If refresh token is also expired then what is the use of keeping check on access_token? Here my user did not do any action in 12 hours (from when the access_token was generated and till access_token got expired), then my user tries to do some action.

Is my refresh_token becoming invalid due to some other reason?

I am occasionally getting token_revoked error as well. I see bot and user access and refresh token in App home page along with their expiration time, Do I get token_expired error, when they are expired (on the App home)?

My refresh tokens are getting expired in 3-4 hours.

I also tried to hit auth.test API you told in parallel in every 30 minutes. But the user refresh token gave me expired_token in 2 hours, other tokens(user_token, bot_token, bot_refresh_token) gave me expired_token message in around 11 hours. How is this possible? What can be the reasons?

Please help. It is very confusing.

zimeg commented 1 week ago

Hey @parth-007 👋 Thanks for looking into token rotation and raising these questions, all are solid (token rotation can no doubt be confusing) but it does sound like something strange is happening during these rotations 🤔

📝 To avoid confusing terms, I'm going to be referencing app settings for these configurations instead of app home.

Note: I have different bot/user scopes in my App settings page and I request different scopes from my code when generating OAuth URL (Which is prompted to the User. Is this okay?

This is fine! The attached scopes will be returned with the token after authenticating, and more can be requested later using a similar method.

Is it correct that for every user, I have unique set of access and refresh tokens? Which are different from the ones listed in the Slack App settings page

For user tokens, yes but the bot token is shared for a team which I think might be related to the strangeness with token expirations and refreshing 🔍

So is it correct behavior that refresh_token is also expired in 12 hours so it is mandatory for the user to ask for refresh in 12 hour limit from where the access token was generated. Any attempt to refresh it after the 12 hours period with the refresh token will result in error?

It sounds like all of the refreshing logic works right when the tokens aren't expired, but I don't believe the refresh token should expire until after it's used... It'd just be the access token that expires after 12 hours.

I'm guessing the invalid_refresh_token might be caused by refreshing with the same refresh token multiple times 👀 Are you using the installationStore from an InstallProvider? I'd be curious to know if your implementation is similar to the @slack/oauth implementation where both the access token, refresh token, and expiration are all updated when needed.

If you can share the snippet you're using, that'd be most helpful! Otherwise, I'm not immediately sure of how this might be happening 🙏

parth-007 commented 1 week ago

Hey @zimeg, Thanks for the quick response!

Let me answer the questions raised by you point-to-point:

For user tokens, yes but the bot token is shared for a team which I think might be related to the strangeness with token expirations and refreshing 🔍

Actually for me, I have a different set of access_token and refresh_token for both bot and user token.

It sounds like all of the refreshing logic works right when the tokens aren't expired, but I don't believe the refresh token should expire until after it's used... It'd just be the access token that expires after 12 hours.

Scenario 1: Actually, there is no need to run the refreshing logic for the 1st hour. Suppose I generated tokens at 2 PM, I added a scheduler which sends a message to a channel every hour. What I've noticed is, it sends message 2-3 times and then when it tries to send the message, there is an error saying token_expired. I rely on expires_in property. I store it when I generated the tokens. When user uses the activity, I check if expires_in is less than the current time, then I perform refresh. But for my case, it breaks in 2-3 hour only. So basically refreshing is never happened. My access token got corrupted.

Scenario 2: Suppose I generated token at 9 AM, I tried to use it to send message at 11 PM. (after 14 hours). My refreshing logic works because expires_in property is less than current date. Now when it goes to refresh the token, I got error refresh_token_expired

I'm guessing the invalid_refresh_token might be caused by refreshing with the same refresh token multiple times 👀 Are you using the installationStore from an InstallProvider? I'd be curious to know if your implementation is similar to the @slack/oauth implementation where both the access token, refresh token, and expiration are all updated when needed.

No, I am not using the installationStore implementation. I have my custom implementation. Please check the code snippet below:

const slackData = {}; // Slack Data like user_token, bot_token, user_token_expires_in, bot_token_expires_in, user_refresh_token, bot_refresh_token
if (new Date() > new Date(slackData.data.user_token_expires_in) || new Date() > new Date(slackData.data.bot_token_expires_in)) {
  secretData = await refreshIntegrationData(slack_auth_id);
} else {
  secretData = slackData.data;
}

_The above is the code which I use to perform any Slack action. It checks for the expiration using the expiresin key. If is is less than current date, I perform refresh, otherwise I use the data from my DB.

Here is the refresh code:

const url = "https://slack.com/api/oauth.v2.access";
const headers = {
  "Content-Type": "application/x-www-form-urlencoded",
};
const { clientId, clientSecret } = config.integrations.slack;
if (new Date() > new Date(integration.data.bot_token_expires_in)) {
  const body = new URLSearchParams({
    client_id: clientId,
    client_secret: clientSecret,
    refresh_token: integration.data.bot_refresh_token,
    grant_type: "refresh_token",
  }).toString();
  const { data } = await postRequest(url, body, headers);
  if (data.ok) {
    integration.data = {
      ...integration.data,
      bot_token: data.access_token,
      bot_refresh_token: data.refresh_token,
      bot_token_expires_in: getExpiresInDate(data.expires_in),
    };
  }
}
if (new Date() > new Date(integration.data.user_token_expires_in)) {
  // Same like above for user token...
}

Please let me know what I am doing wrong or please assist in this case.

zimeg commented 1 week ago

@parth-007 For sure! I'm hopeful we can figure this one out together! Thanks a ton for sharing these snippets and scenarios too- it's so helpful when thinking about this 🙏

I have a different set of access_token and refresh_token for both bot and user token.

This seems expected that the bot and user token are different, and that user tokens are different, but are the bot tokens also different within the same workspace?

So basically refreshing is never happened. My access token got corrupted.

I'd be curious if you have logs before attempting a refresh 👀 I'm thinking that a refresh might be happening at an unexpected time (including never) and am most interested in the logic that starts a token refresh.

One immediate difference that might cause problems is the "scale" of dates and the expiration time. The following line will almost always be true since token expiration is returned in seconds from authorization:

if (new Date() > new Date(slackData.data.user_token_expires_in) || new Date() > new Date(slackData.data.bot_token_expires_in))

Instead of comparing the expires_in values as dates, this value will need to be stored as the offset from the time of authorization. I'd recommend checking out how the default token rotation implementation handles this, but a changed conditional might result from this:

- if (new Date() > new Date(slackData.data.user_token_expires_in))
+ if (new Date() > new Date(slackData.data.user_token_expires_at))

I'm hoping this clears things up a bit more, but please let me know if errors continue to be raised!