Skip to content

fix: Refactor Bitbucket Cloud's OAuth2 host provider to support rotating refresh tokens - #2429

Closed
danielrfraser wants to merge 1 commit into
git-ecosystem:mainfrom
danielrfraser:dfraser/refactor-bitbucket-cloud-oauth-behaviour
Closed

fix: Refactor Bitbucket Cloud's OAuth2 host provider to support rotating refresh tokens#2429
danielrfraser wants to merge 1 commit into
git-ecosystem:mainfrom
danielrfraser:dfraser/refactor-bitbucket-cloud-oauth-behaviour

Conversation

@danielrfraser

@danielrfraser danielrfraser commented Sep 1, 2026

Copy link
Copy Markdown

Updates the BitbucketHostProvider to always store the refresh credentials using the resolved username from bitbucket.

Background

Bitbucket Cloud's OAuth implementation is moving to rotating refresh tokens. These refresh tokens have a TTL of 3 months, but are immediately expired (~10 minutes) after their first usage. This means after each refresh_token grant flow it's expected that clients store the newly issued refresh_token from the OAuth token set.

Another change is that the access_token and refresh_token are far larger than the previous tokens. Regularly exceeding 2500 characters. This will cause issues with CredentialStore which have limits on the credential length. The default for windows wincredman is one such store which has issues. We will be asking customers using GCM on windows (likely via git output and a deprecation notice) that they'll need to change credential store that git credential manager is configured to use to dpapi which doesn't have the size constraints.

Why does the Bitbucket OAuth code in GCM need changing?

When the initial authorization_code grant flow is run, the access_token and refresh_token returned are stored in the configured CredentialStore against the validated Bitbucket username (resolved by calling api.bitbucket.org/2.0/user with the access_token).

When the access_token stored expires, the refresh_token grant flow automatically runs to generate fresh credentials. This flow looks for a refresh_token in the Credential Store stored under the account provided by the request.UserName property.

In some cases this property is null (if the git remote URI configured specifies no username). When the account is null, the Credential Store will return back the first Credential that matches the git host. This will find the refresh_token stored by the original authorization flow. For git remotes configured with a username the CredentialManager will look for a secret belonging exactly to that account.

When, the refresh_token grant flow has concluded and the access_token has been validated the new refresh_token is stored in CredentialStore against the account provided by the remoteUri.GetUserName() function. However, because remoteUri is built with includeUser=False (which is the default) the refresh_token credential will always be stored against a null account in the credential store.

Because of the discrepency between the locations where we store the refresh_token and retrieve it, The refresh_token will only works the first time the refresh_token grant flow is executed. Subsequent refresh_token grant flows will retrieve the original (and now expired) refresh_token - which will be blocked. This causes the auth code will fall into a new authorization grant flow. The broken behaviour would then loop in this manner indefinitely requiring constant reauthorization.

The change

This fix brings the refresh_token grant flow inline with the implementation of the authorization_code grant flow. This stores the refresh_token against the account which is the resolved Bitbucket username. This should mean that regardless of whether you've set your username in the git remote URI, the latest refresh_token will be used to initiate the refresh_token flow.

Rollout

Once the version containing this release is shipped / bundled into Git for Windows, we will be announcing a deprecation and publicly documenting the steps required to continue authing using the OAuth credentials. We will likely run a series of brownouts against the older versions of GCM prior to the complete disablement of the non-rotating refresh tokens.

We will also highlight in our deprecation / announcement (In official Atlassian channels) that any Windows users need to move from using the default CredentialStore of wincredman onto dpapi.

I sparred with the problem and fix with my colleague @mminns who worked on some of the original code some 8+ years ago.

Fixes: #2428

…ting refresh tokens by always storing refresh tokens and access tokens against the resolved Bitbucket username
@danielrfraser
danielrfraser force-pushed the dfraser/refactor-bitbucket-cloud-oauth-behaviour branch from deb1a19 to c4972b7 Compare September 2, 2026 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An upcoming Bitbucket OAuth2 change will break the GCM on Windows when using the default wincredman credentialStore

2 participants