Skip to content

Commit 711a3a9

Browse files
revert: Bitbucket OAuth 1.0 → 2.0 migration (PR #772)
Reverts all changes from the Bitbucket OAuth 2.0 migration in case rollback is needed. Restores OAuth 1.0 signing and original token refresh behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7c1c15f commit 711a3a9

File tree

48 files changed

+303
-443
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+303
-443
lines changed

apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py

Lines changed: 39 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@
88
from codecov_auth.models import Owner
99
from codecov_auth.views.bitbucket import BitbucketLoginView
1010
from shared.torngit.bitbucket import Bitbucket
11-
from shared.torngit.exceptions import (
12-
TorngitClientGeneralError,
13-
)
11+
from shared.torngit.exceptions import TorngitServer5xxCodeError
12+
from utils.encryption import encryptor
1413

1514

1615
def test_get_bitbucket_redirect(client, settings, mocker):
17-
mocked_generate = mocker.patch.object(
16+
mocked_get = mocker.patch.object(
1817
Bitbucket,
19-
"generate_redirect_url",
20-
return_value="https://bitbucket.org/site/oauth2/authorize?client_id=testqmo19ebdkseoby&response_type=code&redirect_uri=http%3A%2F%2Flocalhost&state=teststate",
18+
"generate_request_token",
19+
return_value={
20+
"oauth_token": "testy6r2of6ajkmrub",
21+
"oauth_token_secret": "testzibw5q01scpl8qeeupzh8u9yu8hz",
22+
},
2123
)
2224
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
2325
settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby"
@@ -26,33 +28,30 @@ def test_get_bitbucket_redirect(client, settings, mocker):
2628
res = client.get(url, SERVER_NAME="localhost:8000")
2729
assert res.status_code == 302
2830

29-
assert "_bb_oauth_state" in res.cookies
30-
cookie = res.cookies["_bb_oauth_state"]
31+
assert "_oauth_request_token" in res.cookies
32+
cookie = res.cookies["_oauth_request_token"]
3133
assert cookie.value
3234
assert cookie.get("domain") == settings.COOKIES_DOMAIN
33-
assert cookie.get("secure")
34-
assert cookie.get("samesite") == settings.COOKIE_SAME_SITE
35-
assert cookie.get("max-age") == 300
36-
assert mocked_generate.call_count == 1
37-
# state kwarg was passed through
38-
_, kwargs = mocked_generate.call_args
39-
assert kwargs.get("state") is not None
35+
assert (
36+
res.url
37+
== "https://bitbucket.org/api/1.0/oauth/authenticate?oauth_token=testy6r2of6ajkmrub"
38+
)
39+
mocked_get.assert_called_with(settings.BITBUCKET_REDIRECT_URI)
4040

4141

42-
def test_get_bitbucket_redirect_bitbucket_error(client, settings, mocker):
43-
mocker.patch.object(
44-
Bitbucket,
45-
"generate_redirect_url",
46-
side_effect=TorngitClientGeneralError(400, {}, "bad request"),
42+
def test_get_bitbucket_redirect_bitbucket_unavailable(client, settings, mocker):
43+
mocked_get = mocker.patch.object(
44+
Bitbucket, "generate_request_token", side_effect=TorngitServer5xxCodeError()
4745
)
4846
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
4947
settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby"
5048
settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f"
5149
url = reverse("bitbucket-login")
5250
res = client.get(url, SERVER_NAME="localhost:8000")
5351
assert res.status_code == 302
54-
assert "_bb_oauth_state" not in res.cookies
52+
assert "_oauth_request_token" not in res.cookies
5553
assert res.url == url
54+
mocked_get.assert_called_with(settings.BITBUCKET_REDIRECT_URI)
5655

5756

5857
async def fake_get_authenticated_user():
@@ -111,22 +110,23 @@ async def fake_list_teams():
111110
"generate_access_token",
112111
return_value={
113112
"key": "test6tl3evq7c8vuyn",
114-
"secret": "testrefreshtoken",
113+
"secret": "testdm61tppb5x0tam7nae3qajhcepzz",
115114
},
116115
)
117116
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
118117
settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby"
119118
settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f"
120119
settings.CODECOV_DASHBOARD_URL = "dashboard.value"
121120
settings.COOKIE_SECRET = "aaaaa"
122-
123-
state = "test_state_value_abc123"
124121
url = reverse("bitbucket-login")
122+
oauth_request_token = (
123+
"dGVzdDZ0bDNldnE3Yzh2dXlu|dGVzdGRtNjF0cHBiNXgwdGFtN25hZTNxYWpoY2Vweno="
124+
)
125125
client.cookies = SimpleCookie(
126126
{
127-
"_bb_oauth_state": signing.get_cookie_signer(salt="_bb_oauth_state").sign(
128-
state
129-
)
127+
"_oauth_request_token": signing.get_cookie_signer(
128+
salt="_oauth_request_token"
129+
).sign(encryptor.encode(oauth_request_token).decode())
130130
}
131131
)
132132
mock_create_user_onboarding_metric = mocker.patch(
@@ -135,17 +135,17 @@ async def fake_list_teams():
135135

136136
res = client.get(
137137
url,
138-
{"code": "auth_code_from_bitbucket", "state": state},
138+
{"oauth_verifier": 8519288973, "oauth_token": "test1daxl4jnhegoh4"},
139139
SERVER_NAME="localhost:8000",
140140
)
141141
assert res.status_code == 302
142142
assert res.url == "dashboard.value/bb"
143-
assert "_bb_oauth_state" in res.cookies
144-
cookie = res.cookies["_bb_oauth_state"]
143+
assert "_oauth_request_token" in res.cookies
144+
cookie = res.cookies["_oauth_request_token"]
145145
assert cookie.value == ""
146146
assert cookie.get("domain") == settings.COOKIES_DOMAIN
147147
mocked_get.assert_called_with(
148-
"auth_code_from_bitbucket", settings.BITBUCKET_REDIRECT_URI
148+
"test6tl3evq7c8vuyn", "testdm61tppb5x0tam7nae3qajhcepzz", "8519288973"
149149
)
150150
owner = Owner.objects.get(username="ThiagoCodecov", service="bitbucket")
151151
expected_call = call(
@@ -155,8 +155,13 @@ async def fake_list_teams():
155155
)
156156
assert mock_create_user_onboarding_metric.call_args_list == [expected_call]
157157

158+
assert (
159+
encryptor.decode(owner.oauth_token)
160+
== "test6tl3evq7c8vuyn:testdm61tppb5x0tam7nae3qajhcepzz"
161+
)
158162

159-
def test_get_bitbucket_already_token_no_state_cookie(
163+
164+
def test_get_bitbucket_already_token_no_cookie(
160165
client, settings, mocker, db, mock_redis
161166
):
162167
mocker.patch(
@@ -170,7 +175,7 @@ def test_get_bitbucket_already_token_no_state_cookie(
170175
"generate_access_token",
171176
return_value={
172177
"key": "test6tl3evq7c8vuyn",
173-
"secret": "testrefreshtoken",
178+
"secret": "testdm61tppb5x0tam7nae3qajhcepzz",
174179
},
175180
)
176181
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
@@ -179,39 +184,7 @@ def test_get_bitbucket_already_token_no_state_cookie(
179184
url = reverse("bitbucket-login")
180185
res = client.get(
181186
url,
182-
{"code": "auth_code_from_bitbucket", "state": "some_state"},
183-
SERVER_NAME="localhost:8000",
184-
)
185-
assert res.status_code == 302
186-
assert res.url == "/login/bitbucket"
187-
assert not mocked_get.called
188-
189-
190-
def test_get_bitbucket_state_mismatch(client, settings, mocker, db, mock_redis):
191-
mocked_get = mocker.patch.object(
192-
Bitbucket,
193-
"generate_access_token",
194-
return_value={
195-
"key": "test6tl3evq7c8vuyn",
196-
"secret": "testrefreshtoken",
197-
},
198-
)
199-
settings.BITBUCKET_REDIRECT_URI = "http://localhost"
200-
settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby"
201-
settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f"
202-
settings.COOKIE_SECRET = "aaaaa"
203-
204-
url = reverse("bitbucket-login")
205-
client.cookies = SimpleCookie(
206-
{
207-
"_bb_oauth_state": signing.get_cookie_signer(salt="_bb_oauth_state").sign(
208-
"legit_state"
209-
)
210-
}
211-
)
212-
res = client.get(
213-
url,
214-
{"code": "auth_code_from_bitbucket", "state": "attacker_injected_state"},
187+
{"oauth_verifier": 8519288973, "oauth_token": "test1daxl4jnhegoh4"},
215188
SERVER_NAME="localhost:8000",
216189
)
217190
assert res.status_code == 302

apps/codecov-api/codecov_auth/views/bitbucket.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import base64
12
import logging
2-
import secrets
3+
from urllib.parse import urlencode
34

45
from asgiref.sync import async_to_sync
56
from django.conf import settings
@@ -12,10 +13,8 @@
1213
UserOnboardingMetricsService,
1314
)
1415
from shared.torngit import Bitbucket
15-
from shared.torngit.exceptions import (
16-
TorngitClientGeneralError,
17-
TorngitServerFailureError,
18-
)
16+
from shared.torngit.exceptions import TorngitServerFailureError
17+
from utils.encryption import encryptor
1918

2019
log = logging.getLogger(__name__)
2120

@@ -54,19 +53,23 @@ def redirect_to_bitbucket_step(self, request):
5453
"secret": settings.BITBUCKET_CLIENT_SECRET,
5554
}
5655
)
57-
state = secrets.token_urlsafe(32)
58-
url_to_redirect = repo_service.generate_redirect_url(
59-
settings.BITBUCKET_REDIRECT_URI, state=state
56+
oauth_token_pair = repo_service.generate_request_token(
57+
settings.BITBUCKET_REDIRECT_URI
6058
)
59+
oauth_token = oauth_token_pair["oauth_token"]
60+
oauth_token_secret = oauth_token_pair["oauth_token_secret"]
61+
url_params = urlencode({"oauth_token": oauth_token})
62+
url_to_redirect = f"{Bitbucket._OAUTH_AUTHORIZE_URL}?{url_params}"
6163
response = redirect(url_to_redirect)
64+
data = (
65+
base64.b64encode(oauth_token.encode())
66+
+ b"|"
67+
+ base64.b64encode(oauth_token_secret.encode())
68+
).decode()
6269
response.set_signed_cookie(
63-
"_bb_oauth_state",
64-
state,
70+
"_oauth_request_token",
71+
encryptor.encode(data).decode(),
6572
domain=settings.COOKIES_DOMAIN,
66-
httponly=True,
67-
secure=settings.SESSION_COOKIE_SECURE,
68-
samesite=settings.COOKIE_SAME_SITE,
69-
max_age=300,
7073
)
7174
self.store_to_cookie_utm_tags(response)
7275
return response
@@ -78,13 +81,19 @@ def actual_login_step(self, request):
7881
"secret": settings.BITBUCKET_CLIENT_SECRET,
7982
}
8083
)
81-
expected_state = request.get_signed_cookie("_bb_oauth_state", default=None)
82-
if not expected_state or request.GET.get("state") != expected_state:
83-
log.warning("Bitbucket OAuth state mismatch — possible CSRF attempt")
84+
oauth_verifier = request.GET.get("oauth_verifier")
85+
request_cookie = request.get_signed_cookie("_oauth_request_token", default=None)
86+
if not request_cookie:
87+
log.warning(
88+
"Request arrived with proper url params but not the proper cookies"
89+
)
8490
return redirect(reverse("bitbucket-login"))
85-
code = request.GET.get("code")
91+
request_cookie = encryptor.decode(request_cookie)
92+
cookie_key, cookie_secret = [
93+
base64.b64decode(i).decode() for i in request_cookie.split("|")
94+
]
8695
token = repo_service.generate_access_token(
87-
code, settings.BITBUCKET_REDIRECT_URI
96+
cookie_key, cookie_secret, oauth_verifier
8897
)
8998
user_dict = self.fetch_user_data(token)
9099
user = self.get_and_modify_owner(user_dict, request)
@@ -93,7 +102,7 @@ def actual_login_step(self, request):
93102
redirection_url, user
94103
)
95104
response = redirect(redirection_url)
96-
response.delete_cookie("_bb_oauth_state", domain=settings.COOKIES_DOMAIN)
105+
response.delete_cookie("_oauth_request_token", domain=settings.COOKIES_DOMAIN)
97106
self.login_owner(user, request, response)
98107
log.info("User successfully logged in", extra={"ownerid": user.ownerid})
99108
UserOnboardingMetricsService.create_user_onboarding_metric(
@@ -106,7 +115,7 @@ def get(self, request):
106115
return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login")
107116

108117
try:
109-
if request.GET.get("code"):
118+
if request.GET.get("oauth_verifier"):
110119
log.info("Logging into bitbucket after authorization")
111120
return self.actual_login_step(request)
112121
else:
@@ -115,6 +124,3 @@ def get(self, request):
115124
except TorngitServerFailureError:
116125
log.warning("Bitbucket not available for login")
117126
return redirect(reverse("bitbucket-login"))
118-
except TorngitClientGeneralError:
119-
log.warning("Bitbucket OAuth error during login")
120-
return redirect(reverse("bitbucket-login"))

apps/codecov-api/services/repo_providers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def get_token_refresh_callback(
4242
"""
4343
if owner is None:
4444
return None
45-
if service == Service.BITBUCKET_SERVER:
45+
if service == Service.BITBUCKET or service == Service.BITBUCKET_SERVER:
4646
return None
4747

4848
@sync_to_async

apps/codecov-api/services/tests/test_repo_providers.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def test__is_using_integration_ghapp_covers_some_repos(mock_get_config, db):
111111
"should_have_owner,service",
112112
[
113113
(False, Service.GITHUB.value),
114+
(True, Service.BITBUCKET.value),
114115
(True, Service.BITBUCKET_SERVER.value),
115116
],
116117
)
@@ -121,13 +122,6 @@ def test_token_refresh_callback_none_cases(should_have_owner, service, db):
121122
assert get_token_refresh_callback(owner, service) is None
122123

123124

124-
def test_token_refresh_callback_bitbucket(db):
125-
owner = OwnerFactory(service=Service.BITBUCKET.value)
126-
callback = get_token_refresh_callback(owner, Service.BITBUCKET)
127-
assert callback is not None
128-
assert inspect.iscoroutinefunction(callback)
129-
130-
131125
GITHUB_SENTRY_APP_ID = 4321
132126

133127

apps/worker/helpers/token_refresh.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def get_token_refresh_callback(owner: Owner) -> Callable[[dict], None]:
1919
return None
2020

2121
service = owner.service
22-
if service == "bitbucket_server":
22+
if service == "bitbucket" or service == "bitbucket_server":
2323
return None
2424

2525
async def callback(new_token: dict) -> None:

apps/worker/services/tests/test_repository_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def test_get_repo_provider_service_bitbucket(dbsession):
224224
}
225225
assert res.data == expected_data
226226
assert repo.author.service == "bitbucket"
227-
assert res._on_token_refresh is not None
227+
assert res._on_token_refresh is None
228228
assert res.token == {
229229
"username": repo.author.username,
230230
"key": "testyftq3ovzkb3zmt823u3t04lkrt9w",

0 commit comments

Comments
 (0)