Skip to content

Commit f52b1fb

Browse files
Merge pull request #722 from codecov/dorian/CODE-628/state-in-redis
implement redirection to the provided url after authentication
2 parents 226b622 + 16b250e commit f52b1fb

File tree

12 files changed

+264
-30
lines changed

12 files changed

+264
-30
lines changed

Diff for: codecov/settings_base.py

+6
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@
185185
ARCHIVE_BUCKET_NAME = "codecov"
186186
ENCRYPTION_SECRET = get_config("setup", "encryption_secret")
187187

188+
COOKIE_SAME_SITE = "Lax"
188189
COOKIE_SECRET = get_config("setup", "http", "cookie_secret")
189190
COOKIES_DOMAIN = get_config("setup", "http", "cookies_domain", default=".codecov.io")
190191
SESSION_COOKIE_DOMAIN = get_config(
@@ -228,5 +229,10 @@
228229

229230
IS_ENTERPRISE = get_settings_module() == SettingsModule.ENTERPRISE.value
230231
IS_DEV = get_settings_module() == SettingsModule.DEV.value
232+
231233
DATA_UPLOAD_MAX_MEMORY_SIZE = get_config("setup", "http", "upload_max_memory_size", default=2621440)
232234
FILE_UPLOAD_MAX_MEMORY_SIZE = get_config("setup", "http", "file_upload_max_memory_size", default=2621440)
235+
236+
237+
CORS_ALLOWED_ORIGIN_REGEXES = []
238+
CORS_ALLOWED_ORIGINS = []

Diff for: codecov/settings_dev.py

+6
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,11 @@
3636

3737
CODECOV_DASHBOARD_URL = "http://localhost:3000"
3838

39+
CORS_ALLOWED_ORIGINS = [
40+
CODECOV_DASHBOARD_URL,
41+
"http://localhost",
42+
"http://localhost:9000",
43+
]
44+
3945
COOKIES_DOMAIN = "localhost"
4046
SESSION_COOKIE_DOMAIN = "localhost"

Diff for: codecov/settings_prod.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
CODECOV_DASHBOARD_URL,
4242
"https://gazebo.netlify.app", # to access unreleased URL of gazebo
4343
]
44+
# We are also using the CORS settings to verify if the domain is safe to
45+
# Redirect after authentication, update this setting with care
46+
CORS_ALLOWED_ORIGIN_REGEXES = []
4447

4548
DATA_UPLOAD_MAX_MEMORY_SIZE = 15000000
46-
SILENCED_SYSTEM_CHECKS = ['urls.W002']
49+
SILENCED_SYSTEM_CHECKS = ["urls.W002"]
50+
51+
# Reinforcing the Cookie SameSite configuration to be sure it's Lax in prod
52+
COOKIE_SAME_SITE = "Lax"

Diff for: codecov/settings_staging.py

+4
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@
5656
]
5757

5858
DATA_UPLOAD_MAX_MEMORY_SIZE = 15000000
59+
60+
# Same site is set to none on Staging as we want to be able to call the API
61+
# From Netlify preview deploy
62+
COOKIE_SAME_SITE = "None"

Diff for: codecov_auth/admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def impersonate_owner(self, request, queryset):
2121
"staff_user",
2222
owner.username,
2323
domain=settings.COOKIES_DOMAIN,
24-
samesite="Lax",
24+
samesite=settings.COOKIE_SAME_SITE,
2525
)
2626
return response
2727

Diff for: codecov_auth/tests/unit/views/test_base.py

+86-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,93 @@
1-
from django.test import TestCase, RequestFactory
1+
from django.test import TestCase, RequestFactory, override_settings
2+
from django.core.exceptions import SuspiciousOperation
3+
import pytest
24
from unittest.mock import patch
3-
from codecov_auth.views.base import LoginMixin
5+
6+
from codecov_auth.views.base import LoginMixin, StateMixin
47
from codecov_auth.tests.factories import OwnerFactory
58

69

10+
def set_up_mixin(to=None):
11+
query_string = {"to": to} if to else None
12+
mixin = StateMixin()
13+
mixin.request = RequestFactory().get("", query_string)
14+
mixin.service = "github"
15+
return mixin
16+
17+
18+
def test_generate_state_without_redirection_url(mock_redis):
19+
mixin = set_up_mixin()
20+
state = mixin.generate_state()
21+
assert (
22+
mock_redis.get(f"oauth-state-{state}").decode("utf-8")
23+
== "http://localhost:3000/gh"
24+
)
25+
26+
27+
def test_generate_state_with_path_redirection_url(mock_redis):
28+
mixin = set_up_mixin("/gh/codecov")
29+
state = mixin.generate_state()
30+
assert mock_redis.get(f"oauth-state-{state}").decode("utf-8") == "/gh/codecov"
31+
32+
33+
@override_settings(CORS_ALLOWED_ORIGINS=["https://app.codecov.io"])
34+
def test_generate_state_with_safe_domain_redirection_url(mock_redis):
35+
mixin = set_up_mixin("https://app.codecov.io/gh/codecov")
36+
state = mixin.generate_state()
37+
assert (
38+
mock_redis.get(f"oauth-state-{state}").decode("utf-8")
39+
== "https://app.codecov.io/gh/codecov"
40+
)
41+
42+
43+
@override_settings(CORS_ALLOWED_ORIGINS=[])
44+
@override_settings(CORS_ALLOWED_ORIGIN_REGEXES=[r"^(https:\/\/)?(.+)\.codecov\.io$"])
45+
def test_generate_state_with_safe_domain_regex_redirection_url(mock_redis):
46+
mixin = set_up_mixin("https://app.codecov.io/gh/codecov")
47+
state = mixin.generate_state()
48+
assert (
49+
mock_redis.get(f"oauth-state-{state}").decode("utf-8")
50+
== "https://app.codecov.io/gh/codecov"
51+
)
52+
53+
54+
@override_settings(CORS_ALLOWED_ORIGINS=[])
55+
@override_settings(CORS_ALLOWED_ORIGIN_REGEXES=[])
56+
def test_generate_state_with_unsafe_domain(mock_redis):
57+
mixin = set_up_mixin("http://hacker.com/i-steal-cookie")
58+
state = mixin.generate_state()
59+
assert mock_redis.keys("*") != []
60+
assert (
61+
mock_redis.get(f"oauth-state-{state}").decode("utf-8")
62+
== "http://localhost:3000/gh"
63+
)
64+
65+
66+
@override_settings(CORS_ALLOWED_ORIGINS=[])
67+
@override_settings(CORS_ALLOWED_ORIGIN_REGEXES=[])
68+
def test_generate_state_when_wrong_url(mock_redis):
69+
mixin = set_up_mixin("http://localhost:]/")
70+
state = mixin.generate_state()
71+
assert mock_redis.keys("*") != []
72+
assert (
73+
mock_redis.get(f"oauth-state-{state}").decode("utf-8")
74+
== "http://localhost:3000/gh"
75+
)
76+
77+
78+
def test_get_redirection_url_from_state_no_state(mock_redis):
79+
mixin = set_up_mixin()
80+
with pytest.raises(SuspiciousOperation):
81+
mixin.get_redirection_url_from_state("not exist")
82+
83+
84+
def test_get_redirection_url_from_state_give_url(mock_redis):
85+
mixin = set_up_mixin()
86+
mock_redis.set(f"oauth-state-abc", "http://localhost/gh/codecov")
87+
assert mixin.get_redirection_url_from_state("abc") == "http://localhost/gh/codecov"
88+
assert mock_redis.get(f"oauth-state-abc") is None
89+
90+
791
class LoginMixinTests(TestCase):
892
def setUp(self):
993
self.mixin_instance = LoginMixin()

Diff for: codecov_auth/tests/unit/views/test_github.py

+34-11
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,48 @@
1111
from codecov_auth.models import Owner
1212

1313

14-
def test_get_github_redirect(client):
14+
def _get_state_from_redis(mock_redis):
15+
key_redis = mock_redis.keys("*")[0].decode()
16+
return key_redis.replace("oauth-state-", "")
17+
18+
19+
def test_get_github_redirect(client, mock_redis):
1520
url = reverse("github-login")
1621
res = client.get(url)
22+
state = _get_state_from_redis(mock_redis)
1723
assert res.status_code == 302
1824
assert (
1925
res.url
20-
== "https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook&client_id=3d44be0e772666136a13"
26+
== f"https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook&client_id=3d44be0e772666136a13&state={state}"
2127
)
2228

2329

24-
def test_get_github_redirect_with_ghpr_cookie(client, settings):
30+
def test_get_github_redirect_with_ghpr_cookie(client, mock_redis, settings):
2531
settings.COOKIES_DOMAIN = ".simple.site"
2632
client.cookies = SimpleCookie({"ghpr": "true"})
2733
url = reverse("github-login")
2834
res = client.get(url)
35+
state = _get_state_from_redis(mock_redis)
2936
assert res.status_code == 302
3037
assert (
3138
res.url
32-
== "https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=3d44be0e772666136a13"
39+
== f"https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=3d44be0e772666136a13&state={state}"
3340
)
3441
assert "ghpr" in res.cookies
3542
ghpr_cooke = res.cookies["ghpr"]
3643
assert ghpr_cooke.value == "true"
3744
assert ghpr_cooke.get("domain") == ".simple.site"
3845

3946

40-
def test_get_github_redirect_with_private_url(client, settings):
47+
def test_get_github_redirect_with_private_url(client, mock_redis, settings):
4148
settings.COOKIES_DOMAIN = ".simple.site"
4249
url = reverse("github-login")
4350
res = client.get(url, {"private": "true"})
51+
state = _get_state_from_redis(mock_redis)
4452
assert res.status_code == 302
4553
assert (
4654
res.url
47-
== "https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=3d44be0e772666136a13"
55+
== f"https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=3d44be0e772666136a13&state={state}"
4856
)
4957
assert "ghpr" in res.cookies
5058
ghpr_cooke = res.cookies["ghpr"]
@@ -118,8 +126,10 @@ async def is_student(*args, **kwargs):
118126
as_tuple=mocker.MagicMock(return_value=("a", "b"))
119127
),
120128
)
129+
121130
url = reverse("github-login")
122-
res = client.get(url, {"code": "aaaaaaa"})
131+
mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh")
132+
res = client.get(url, {"code": "aaaaaaa", "state": "abc"})
123133
assert res.status_code == 302
124134
assert "github-token" in res.cookies
125135
assert "github-username" in res.cookies
@@ -181,15 +191,25 @@ def test_get_github_already_with_code_github_error(
181191
async def helper_func(*args, **kwargs):
182192
raise TorngitClientGeneralError(403, "response", "message")
183193

194+
mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh")
195+
184196
mocker.patch.object(Github, "get_authenticated_user", side_effect=helper_func)
185197
url = reverse("github-login")
186-
res = client.get(url, {"code": "aaaaaaa"})
198+
res = client.get(url, {"code": "aaaaaaa", "state": "abc"})
187199
assert res.status_code == 302
188200
assert "github-token" not in res.cookies
189201
assert "github-username" not in res.cookies
190202
assert res.url == "/"
191203

192204

205+
def test_state_not_known(client, mocker, db, mock_redis, settings):
206+
url = reverse("github-login")
207+
res = client.get(url, {"code": "aaaaaaa", "state": "doesnt exist"})
208+
assert res.status_code == 400
209+
assert "github-token" not in res.cookies
210+
assert "github-username" not in res.cookies
211+
212+
193213
def test_get_github_already_with_code_with_email(
194214
client, mocker, db, mock_redis, settings
195215
):
@@ -226,8 +246,9 @@ async def is_student(*args, **kwargs):
226246
as_tuple=mocker.MagicMock(return_value=("a", "b"))
227247
),
228248
)
249+
mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh")
229250
url = reverse("github-login")
230-
res = client.get(url, {"code": "aaaaaaa"})
251+
res = client.get(url, {"code": "aaaaaaa", "state": "abc"})
231252
assert res.status_code == 302
232253
assert "github-token" in res.cookies
233254
assert "github-username" in res.cookies
@@ -281,8 +302,9 @@ async def is_student(*args, **kwargs):
281302
as_tuple=mocker.MagicMock(return_value=("a", "b"))
282303
),
283304
)
305+
mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh")
284306
url = reverse("github-login")
285-
res = client.get(url, {"code": "aaaaaaa"})
307+
res = client.get(url, {"code": "aaaaaaa", "state": "abc"})
286308
assert res.status_code == 302
287309
assert "github-token" in res.cookies
288310
assert "github-username" in res.cookies
@@ -345,7 +367,8 @@ async def is_student(*args, **kwargs):
345367
),
346368
)
347369
url = reverse("github-login")
348-
res = client.get(url, {"code": "aaaaaaa"})
370+
mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh")
371+
res = client.get(url, {"code": "aaaaaaa", "state": "abc"})
349372
assert res.status_code == 302
350373
assert "github-token" in res.cookies
351374
assert "github-username" in res.cookies

Diff for: codecov_auth/tests/unit/views/test_gitlab.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
from codecov_auth.models import Session
1010

1111

12-
def test_get_gitlab_redirect(client, settings, mocker):
12+
def _get_state_from_redis(mock_redis):
13+
key_redis = mock_redis.keys("*")[0].decode()
14+
return key_redis.replace("oauth-state-", "")
15+
16+
17+
def test_get_gitlab_redirect(client, settings, mock_redis, mocker):
1318
mocker.patch(
1419
"codecov_auth.views.gitlab.uuid4",
1520
return_value=UUID("fbdf86c6c8d64ed1b814e80b33df85c9"),
@@ -23,10 +28,11 @@ def test_get_gitlab_redirect(client, settings, mocker):
2328
settings.GITLAB_REDIRECT_URI = "http://localhost/login/gitlab"
2429
url = reverse("gitlab-login")
2530
res = client.get(url, SERVER_NAME="localhost:8000")
31+
state = _get_state_from_redis(mock_redis)
2632
assert res.status_code == 302
2733
assert (
2834
res.url
29-
== "https://gitlab.com/oauth/authorize?response_type=code&client_id=testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Fgitlab&state=fbdf86c6c8d64ed1b814e80b33df85c9"
35+
== f"https://gitlab.com/oauth/authorize?response_type=code&client_id=testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Fgitlab&state={state}"
3036
)
3137

3238

@@ -70,7 +76,8 @@ async def helper_list_teams_func(*args, **kwargs):
7076
),
7177
)
7278
url = reverse("gitlab-login")
73-
res = client.get(url, {"code": "aaaaaaa"})
79+
mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gl")
80+
res = client.get(url, {"code": "aaaaaaa", "state": "abc"})
7481
assert res.status_code == 302
7582
assert "gitlab-token" in res.cookies
7683
assert "gitlab-username" in res.cookies
@@ -97,7 +104,8 @@ async def helper_func(*args, **kwargs):
97104

98105
mocker.patch.object(Gitlab, "get_authenticated_user", side_effect=helper_func)
99106
url = reverse("gitlab-login")
100-
res = client.get(url, {"code": "aaaaaaa"})
107+
mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gl")
108+
res = client.get(url, {"code": "aaaaaaa", "state": "abc"})
101109
assert res.status_code == 302
102110
assert "gitlab-token" not in res.cookies
103111
assert "gitlab-username" not in res.cookies

0 commit comments

Comments
 (0)