Skip to content

Commit 651e997

Browse files
committed
[lichess] Check the CSRF state, rather than ignoring that aspect of OAuth2 for now
1 parent b81ea1a commit 651e997

File tree

3 files changed

+33
-9
lines changed

3 files changed

+33
-9
lines changed

src/apps/lichess_bridge/authentication.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
from authlib.common.security import generate_token
1616
from authlib.integrations.requests_client import OAuth2Session
1717
from django.conf import settings
18+
from django.core.exceptions import SuspiciousOperation
1819
from django.urls import reverse
1920

2021
if TYPE_CHECKING:
2122
from typing import Self
2223

24+
from django.http import HttpRequest
25+
2326
from .models import LichessAccessToken
2427

2528
LICHESS_OAUTH2_SCOPES = ("board:play",)
@@ -104,7 +107,7 @@ def get_lichess_token_retrieval_via_oauth2_process_starting_url(
104107
) -> str:
105108
lichess_authorization_endpoint = f"{settings.LICHESS_HOST}/oauth"
106109

107-
client = _get_lichess_client()
110+
client = _get_lichess_oauth2_client()
108111
uri, state = client.create_authorization_url(
109112
lichess_authorization_endpoint,
110113
response_type="code",
@@ -117,14 +120,27 @@ def get_lichess_token_retrieval_via_oauth2_process_starting_url(
117120
return uri
118121

119122

120-
def extract_lichess_token_from_oauth2_callback_url(
123+
def check_csrf_state_from_oauth2_callback(
124+
*, request: "HttpRequest", context: LichessTokenRetrievalProcessContext
125+
):
126+
"""
127+
Raises a SuspiciousOperation if the state from the request's query string
128+
doesn't match the state from the short-lived cookie.
129+
"""
130+
csrf_state_from_request = request.GET["state"]
131+
csrf_state_from_short_lived_cookie = context.csrf_state
132+
if csrf_state_from_short_lived_cookie != csrf_state_from_request:
133+
raise SuspiciousOperation("OAuth2 CSRF state mismatch")
134+
135+
136+
def fetch_lichess_token_from_oauth2_callback(
121137
*,
122138
authorization_callback_response_url: str,
123139
context: LichessTokenRetrievalProcessContext,
124140
) -> LichessToken:
125141
lichess_token_endpoint = f"{settings.LICHESS_HOST}/api/token"
126142

127-
client = _get_lichess_client()
143+
client = _get_lichess_oauth2_client()
128144
token_as_dict = client.fetch_token(
129145
lichess_token_endpoint,
130146
authorization_response=authorization_callback_response_url,
@@ -146,7 +162,7 @@ def _get_lichess_oauth2_zakuchess_redirect_uri(
146162
)
147163

148164

149-
def _get_lichess_client() -> OAuth2Session:
165+
def _get_lichess_oauth2_client() -> OAuth2Session:
150166
return OAuth2Session(
151167
client_id=settings.LICHESS_CLIENT_ID,
152168
code_challenge_method="S256",

src/apps/lichess_bridge/tests/test_views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def test_lichess_homepage_with_access_token_smoke_test(
2727
"username": "ChessChampion"
2828
}
2929

30-
client.cookies["lichess.access_token"] = "lio_123456"
30+
client.cookies["lichess.access_token"] = "lio_123456789"
3131
response = client.get("/lichess/")
3232
assert response.status_code == HTTPStatus.OK
3333

@@ -36,4 +36,4 @@ def test_lichess_homepage_with_access_token_smoke_test(
3636
assert "Log out from Lichess" in response_html
3737
assert "ChessChampion" in response_html
3838

39-
create_lichess_api_client_mock.assert_called_once_with("lio_123456")
39+
create_lichess_api_client_mock.assert_called_once_with("lio_123456789")

src/apps/lichess_bridge/views.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from . import cookie_helpers
88
from .authentication import (
99
LichessTokenRetrievalProcessContext,
10-
extract_lichess_token_from_oauth2_callback_url,
10+
check_csrf_state_from_oauth2_callback,
11+
fetch_lichess_token_from_oauth2_callback,
1112
get_lichess_token_retrieval_via_oauth2_process_starting_url,
1213
)
1314
from .components.pages.lichess import (
@@ -69,7 +70,14 @@ def lichess_webhook_oauth2_token_callback(request: "HttpRequest") -> HttpRespons
6970
# TODO: Do something with that error
7071
return redirect("lichess_bridge:homepage")
7172

72-
token = extract_lichess_token_from_oauth2_callback_url(
73+
# We have to check the "CSRF state":
74+
# ( https://stack-auth.com/blog/oauth-from-first-principles#attack-4 )
75+
check_csrf_state_from_oauth2_callback(
76+
request=request, context=lichess_oauth2_process_context
77+
)
78+
79+
# Ok, now let's fetch an API access token from Lichess!
80+
token = fetch_lichess_token_from_oauth2_callback(
7381
authorization_callback_response_url=request.get_full_path(),
7482
context=lichess_oauth2_process_context,
7583
)
@@ -80,7 +88,7 @@ def lichess_webhook_oauth2_token_callback(request: "HttpRequest") -> HttpRespons
8088
cookie_helpers.delete_oauth2_token_retrieval_context_from_cookies(response)
8189

8290
# Now that we have an access token to interact with Lichess' API on behalf
83-
# of the user, let's store it into a HTTP-only cookie:
91+
# of the user, let's store it into a long-lived HTTP-only cookie:
8492
cookie_helpers.store_lichess_api_access_token_in_response_cookie(
8593
token=token,
8694
response=response,

0 commit comments

Comments
 (0)