4 Aug 2021 - Action required - Deprecating persistent refresh tokens

Update: We’ve heard your feedback and have extended the deprecation period through to the 1st November 2021.

What is changing?

We’re rolling out a breaking change for OAuth 2.0 integrations - formerly known as OAuth 2.0 (3LO) apps. This affects all OAuth 2.0 integrations that use the offline_access scope to enable refresh tokens.

We’re migrating away from the current persistent refresh token to rotating refresh tokens. These are single use refresh tokens with a 30 day expiry time.

You’ll need to update your integrations to handle the additional the additional fields returned with a new refresh token. Learn more about rotating refresh tokens.

Why is it changing?

OAuth 2.0 integrations that require the offline_access scope have an increased risk when it comes to their access tokens. A persistent refresh token does not expire and is able to request new access tokens for a long period of time.

Rotating refresh tokens issue a new, limited life refresh token each time they are used. This mechanism improves on single persistent refresh tokens by reducing the period in which a refresh token can be compromised and used to obtain a valid access token.

What do I need to do?

Firstly, consider if your app really requires offline_access . If your app requires ongoing access you’ll be able to work with both the persistent and rotating methods during the deprecation window.

You can enable rotating refresh tokens from the developer console, like this:

  1. Select your integration in the developer console.
  2. Select Authorization .
  3. Select Use rotating refresh tokens from the refresh token options.
  4. Save your changes.

By when do I need to do it?

From Aug 4, 2021 persistent refresh tokens are deprecated. All new OAuth 2.0 integrations use rotating refresh tokens.

During the deprecation window you’ll be able to switch between both refresh token behaviors in the developer console.

From Nov 1, 2021 all OAuth 2.0 integrations must use rotating refresh tokens and the refresh token options in the developer console are removed.

3 Likes

@SamWilsonAtlassian thanks for the announcement!

Although I really appreciate the change, this is a really really really small notice period. You’re basically deprecating it at the time of the announcement. Which means that OAuth apps have 30 days to implement a fix during a vacation period.

I would strongly recommend considering an extension to Oct 1st to allow developers to adjust their apps. Otherwise, a lot of these OAuth apps might break.

Something something don’t f*** the customer :wink:

14 Likes

https://developer.atlassian.com/platform/marketplace/atlassian-rest-api-policy/#deprecation-policy states that there is a 6 month deprecation period unless it is a critical vulnerability (which I can’t imagine this is). This notice period doesn’t even give us chance to test our implementations. Please consider extending the deprecation period before removing this.

14 Likes

Hey @remie and @jmort, we hear you and everyone who liked your comments. Due to this being a potential vulnerability and a security concern, we were keen to roll out this change, but at the same time, we understand that one month is fairly tight. We took this back for reevaluation and in this case, based on the threat level, we adjusted the vulnerability ranking.

As such, we’ve decided to extend the deprecation period of the persistent refresh tokens to Nov 1st, 2021, giving everyone more time to update their apps.

The change will be reflected in the original post soon.

6 Likes

Hi @Nir, thanks for the update and extended period. Much appreciated.

edit: I would guess that you can know all of the apps that are affected by this. I think it would be worth contacting via email as well as this post. My guess is that many developers won’t be watching posts here that have OAuth apps.

5 Likes

@SamWilsonAtlassian

Just confirming this change doesn’t apply to connect apps built on ACE?

Cheers,

Rhys

@RhysDiab, no this change has nothing to do with ACE or Atlassian Connect apps. This change only affects OAuth 2.0 (3LO) based apps.

3 Likes

@SamWilsonAtlassian we have started working on this breaking change but find it rather difficult to understand how rotating refresh tokens work and in particular how we should change our implementation.

I have found the following resources which explain rotating refresh tokens in more detail, in case anyone else is looking for more context:

As I understand, the expiry time of 30 days for rotating refresh tokens implies that with this change if a user does not interact with our app/the API for more than 30 days the last issued refresh token expires and they have to go through the entire authorization flow from the beginning again. Could you confirm that this is correct?

One more question regarding the API response from https://auth.atlassian.com/oauth/token when fetching a new access token. Does the expires_in time (in seconds I assume) refer to the access token expiry, the refresh token expiry, or do they both have the same lifetime? The example in the FAQ indicates 2592000. If this is indeed a value in seconds then that translates to exactly 30 days which seems rather long for access token expiry.

Feedback regarding this change notice
IMHO implementing a workable Atlassian OAuth 2 client is a challenge on its own. The documentation is rather basic, without explanation of expected API responses or status codes. Additionally, the lack of any sort of reference implementation makes understanding these changes even more difficult. These circumstances pose the biggest security risk of them all because each and every vendor will have to create their own implementation of the Atlassian OAuth2 protocol (including all the accessible resource quirks). Please have a look at https://github.com/googleapis/google-auth-library-nodejs as an example of a well-documented and tested OAuth 2 client reference implementation.

Regarding this notice specifically, it has a section “What do I need to do?” which simply does not explain what is to be done. It only explains how to enable rotating access tokens. In the section " What is changing?" the only other explanation is:

You’ll need to update your integrations to handle the additional the additional fields returned with a new refresh token.

I wish there was a bit more effort put into explaining how this change works rather than just updating a few sentences in an FAQ. I fear the security fix mostly becomes a new security issue.

4 Likes

I started coding against the API recently and so started with rotating refresh tokens right away. But I run into the problem that after the first round of refresh, neither the original refresh token nor the one returned in the first refresh round work anymore. I fear I’ll have to switch to persistent refresh tokens for now.

To reproduce:

  1. Perform OAuth flow including offline_access as scope to get the initial refresh token.
  2. First refresh round:
curl --request POST \
  --url 'https://auth.atlassian.com/oauth/token' \
  --header 'Content-Type: application/json' \
  --data '{ "grant_type": "refresh_token", "client_id": "ID", "client_secret": "SECRET", "refresh_token": "ORIGINAL_REFRESH_TOKEN" }'

Response:

{"access_token":"NEW_ACCESS_TOKEN","refresh_token":"NEW_REFRESH_TOKEN","scope":"YOUR_SCOPES offline_access","expires_in":3600,"token_type":"Bearer"}
  1. Send the same request as in 2, just with ORIGINAL_REFRESH_TOKEN replaced with NEW_REFRESH_TOKEN.
    Result:
{"error":"invalid_grant","error_description":"Unknown or invalid refresh token."}
  1. Maybe it didn’t actually rotate? Let’s see. Send the exact same request as in 2. Result again:
{"error":"invalid_grant","error_description":"Unknown or invalid refresh token."}

Hi,
You won’t get a new "refresh_token":"NEW_REFRESH_TOKEN" in the response for persistent refresh tokens. So, the token actually rotated. And your ORIGINAL_REFRESH_TOKEN is no longer valid.
I am wondering why the request withNEW_REFRESH_TOKEN failed. Can you please verify that there are no scope changes in YOUR_SCOPES and refresh token is NEW_REFRESH_TOKEN?

Just to note here that, once your refresh token has rotated, NEW_REFRESH_TOKEN is the valid refresh token. If you now edit your app and switch to persistent refresh tokens, NEW_REFRESH_TOKEN is the refresh token you have to use not the ORIGINAL_REFRESH_TOKEN.
Please keep us posted if the issue persists.

1 Like

Hey @NusratSultana,

Thanks for your reply. I verified that there are no scope changes. (Note that YOUR_SCOPES is in the Atlassian response, so I actually can’t change it by accident.) I also verified that I used NEW_REFRESH_TOKEN.

I was not able to reproduce this now with the steps provided above. But it does reproduce with the following steps:

  1. Perform OAuth flow including offline_access as scope to get the initial refresh token.
  2. First refresh round:
curl --request POST \
  --url 'https://auth.atlassian.com/oauth/token' \
  --header 'Content-Type: application/json' \
  --data '{ "grant_type": "refresh_token", "client_id": "ID", "client_secret": "SECRET", "refresh_token": "ORIGINAL_REFRESH_TOKEN" }'

Response:

{"access_token":"NEW_ACCESS_TOKEN","refresh_token":"NEW_REFRESH_TOKEN","scope":"YOUR_SCOPES offline_access","expires_in":3600,"token_type":"Bearer"}
  1. Wait 20 seconds.
  2. Send the request from 2 with ORIGINAL_REFRESH_TOKEN again.
    Response:
{"error":"invalid_grant","error_description":"Unknown or invalid refresh token."}
  1. Send the same request as in 2, just with ORIGINAL_REFRESH_TOKEN replaced with NEW_REFRESH_TOKEN.
    Response:
{"error":"invalid_grant","error_description":"Unknown or invalid refresh token."}

If you leave out steps 3 and 4, it works. With those steps, it fails. Frankly, I’m not 100% sure whether I might not have actually performed steps 3 and 4 a few days ago when I made my original post to verify that the original refresh token got invalidated. That a failed attempt to use an old refresh token invalidates the new refresh token is surprising to me.

Another surprising behavior is that if you leave out step 3, step 4 succeeds. More generally, if you send refresh requests in quick succession with the same refresh token, you get multiple success responses with all different new refresh tokens. I just got 4 different refresh tokens when sending the same original refresh token 4 times in about 4 seconds. For me, the original refresh token starts being rejected about 5 seconds after I exchanged it for the first time. If I don’t send the original refresh token after 5 seconds, i.e. I don’t send any failing requests, then I observe that the first of the new refresh tokens is valid (which is tricky to find out because testing another new refresh token invalidates the first new refresh token, leaving you with no valid refresh token).

I wonder whether this is intended behavior to support retries or whether it’s an artifact of your implementation. Supporting retries certainly would make sense IMO: Imagine I need to refresh the token but then storing the new refresh token fails on my side. So if it’s intended that an old refresh token is accepted for some time after being refreshed, I would suggest 3 things to make it viable for clients:
A. Give the client more time than just 5 seconds. Maybe the retry would depend on the end-user retrying or maybe there is a queuing system with back-off in between, so 5 seconds might not suffice.
B. Ideally, return the same new refresh token (or make all returned refresh tokens valid).
C. If B is not possible, make the last instead of the first new refresh token the one that is valid.

Hi @DanielSadilek ,
The behaviour you are describing at first scenario is because of automatic reuse detection. Once you get a NEW_REFRESH_TOKEN (rotating), any usage of an earlier refresh token will be considered as a lost/stolen refresh token. Hence to prevent such malicious attack both the refresh tokens are invalidated and user is forced to re-consent.

The second scenario where you are able to use same refresh token multiple time is because of reuse interval also known as leeway interval. Atlassian Auth0 configuration leeway interval is 3s.

Following is the definition of leeway from auth0 website,

This interval helps to avoid concurrency issues when exchanging the rotating refresh token multiple times within a given timeframe. During the leeway window the breach detection features don’t apply and a new rotating refresh token is issued. Only the previous token can be reused; if the second-to-last one is exchanged, breach detection will be triggered.

More details: Configuring reuse interval

1 Like

Hey @NusratSultana, thanks for the excellent explanations! This resolves the issue for me.

Is there any more detail available on how to migrate to rotating refresh tokens from persisting refresh tokens? The flow makes sense going forward, but it’s not clear how to do the initial switch.

If I switch my app’s setting over from persistent to rotating refresh tokens, what exactly happens?

  • Do the existing persistent refresh tokens get revoked? Will all of our users need to go through the auth process again to ‘switch’ from persistent to rotating refresh tokens?
  • The persistent refresh tokens at the moment do not return a new refresh token when exchanged for an access token. Will the token call suddenly start returning access and refresh token pairs? If not, how can I get a rotating refresh token when I have a persistent refresh token?

Hi @CassHill ,
When you switch to rotating refresh token, you will start getting new refresh token and access token pairs with every access token request. This new refresh token will invalidate all previous rotating refresh tokens along with the persistent refresh tokens.

If your app is able to store and use the new refresh token, the user will not experience anything different. No re-consent is required. For this to happen, you have to update your app code, to update the exisiting refresh token with the new refresh token returned.

1 Like