Skip to content

Commit a42aa32

Browse files
feat: enhance login process with CSRF token
1 parent 76fe36b commit a42aa32

File tree

4 files changed

+109
-97
lines changed

4 files changed

+109
-97
lines changed

src/commands/login.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@
55

66
def login():
77
"""Login to LeetCode"""
8-
# First try to use saved session
98
if auth_manager.is_authenticated:
109
saved_session = auth_manager.session_manager.load_session()
1110
if saved_session:
1211
typer.echo(typer.style(f"Already logged in as {saved_session['user_name']}!", fg=typer.colors.GREEN))
1312
return
1413

15-
typer.echo(typer.style("To login, you'll need your LEETCODE_SESSION token:", fg=typer.colors.YELLOW))
14+
typer.echo(typer.style("To login, you'll need both CSRF and LEETCODE_SESSION tokens:", fg=typer.colors.YELLOW))
1615
typer.echo("\n1. Open LeetCode in your browser")
1716
typer.echo("2. Press F12 to open Developer Tools")
1817
typer.echo("3. Go to Application tab > Cookies > leetcode.com")
19-
typer.echo("4. Find and copy the 'LEETCODE_SESSION' value\n")
18+
typer.echo("4. Find and copy both 'csrftoken' and 'LEETCODE_SESSION' values\n")
2019

21-
leetcode_session = typer.prompt("Please enter your LEETCODE_SESSION token")
20+
csrf_token = typer.prompt("Please enter your CSRF token")
21+
csrf_result = auth_manager.verify_csrf_token(csrf_token)
22+
23+
if not csrf_result["success"]:
24+
typer.echo(typer.style(f"\n✗ CSRF verification failed: {csrf_result['message']}", fg=typer.colors.RED))
25+
return
2226

23-
result = auth_manager.login_with_session(leetcode_session)
27+
leetcode_session = typer.prompt("Please enter your LEETCODE_SESSION token")
28+
result = auth_manager.login_with_session(csrf_token, leetcode_session)
2429

2530
if result["success"]:
2631
typer.echo(typer.style(f"\n✓ Successfully logged in as {result['user_name']}!", fg=typer.colors.GREEN))

src/server/auth.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict
1+
from typing import Dict, Any
22
import requests
33
from .session_manager import SessionManager
44

@@ -14,21 +14,56 @@ def _load_saved_session(self):
1414
"""Try to load and validate saved session"""
1515
saved_session = self.session_manager.load_session()
1616
if saved_session:
17-
result = self.login_with_session(saved_session['session_token'])
17+
result = self.login_with_session(saved_session["csrftoken"], saved_session['session_token'])
1818
return result["success"]
1919
return False
2020

21-
def login_with_session(self, leetcode_session: str) -> Dict[str, any]: # type: ignore
22-
"""
23-
Login to LeetCode using LEETCODE_SESSION token.
24-
Returns a dictionary with success status and message.
25-
"""
21+
def verify_csrf_token(self, csrf_token: str) -> Dict[str, Any]:
22+
"""Verify CSRF token by making a request to LeetCode's GraphQL endpoint"""
2623
try:
24+
if not csrf_token:
25+
return {"success": False, "message": "CSRF token is required"}
26+
27+
self.session.cookies.set('csrftoken', csrf_token, domain='leetcode.com')
28+
29+
response = self.session.post(
30+
f"{self.BASE_URL}/graphql",
31+
json={
32+
"query": "query { userStatus { isSignedIn username } }"
33+
},
34+
headers={
35+
'X-CSRFToken': csrf_token,
36+
'Accept': 'application/json',
37+
'Content-Type': 'application/json',
38+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
39+
}
40+
)
41+
42+
self.session.cookies.clear(domain='leetcode.com')
43+
44+
if response.status_code == 200:
45+
data = response.json()
46+
if 'errors' not in data:
47+
return {"success": True, "message": "CSRF token verified"}
48+
49+
return {"success": False, "message": "Invalid CSRF token"}
50+
51+
except Exception as e:
52+
return {"success": False, "message": f"CSRF verification error: {str(e)}"}
53+
54+
def login_with_session(self, csrf_token: str, leetcode_session: str) -> Dict[str, Any]:
55+
"""Login using verified CSRF token and session token"""
56+
try:
57+
58+
if not csrf_token or not leetcode_session:
59+
return {"success": False, "message": "Both CSRF and LEETCODE_SESSION tokens are required"}
60+
2761
self.session.cookies.set('LEETCODE_SESSION', leetcode_session, domain='leetcode.com')
2862

2963
response = self.session.get(
3064
f"{self.BASE_URL}/api/problems/all/",
3165
headers={
66+
'X-CSRFToken': csrf_token,
3267
'Accept': 'application/json',
3368
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
3469
}
@@ -38,25 +73,15 @@ def login_with_session(self, leetcode_session: str) -> Dict[str, any]: # type: i
3873
user_data = response.json()
3974
if user_data.get('user_name'):
4075
self.is_authenticated = True
41-
# Save the valid session
42-
self.session_manager.save_session(leetcode_session, user_data['user_name'])
76+
self.session_manager.save_session(csrf_token, leetcode_session, user_data['user_name'])
4377
return {
4478
"success": True,
4579
"message": "Successfully logged in",
4680
"user_name": user_data['user_name']
4781
}
48-
else:
49-
self.session_manager.clear_session()
50-
return {
51-
"success": False,
52-
"message": "Invalid session token"
53-
}
54-
else:
55-
self.session_manager.clear_session()
56-
return {
57-
"success": False,
58-
"message": f"Login failed with status code: {response.status_code}"
59-
}
82+
83+
self.session_manager.clear_session()
84+
return {"success": False, "message": "Invalid session credentials"}
6085

6186
except Exception as e:
6287
self.session_manager.clear_session()

src/server/session_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ def _ensure_config_dir(self):
1414
"""Ensure the config directory exists"""
1515
self.config_dir.mkdir(parents=True, exist_ok=True)
1616

17-
def save_session(self, session_token: str, user_name: str):
17+
def save_session(self, csrftoken: str, session_token: str, user_name: str):
1818
"""Save the session token and username to file"""
1919
data = {
20+
'csrftoken': csrftoken,
2021
'session_token': session_token,
2122
'user_name': user_name
2223
}

src/server/solution_manager.py

Lines changed: 51 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ def _clean_session_cookies(self):
1515
if not hasattr(self.session, 'cookies'):
1616
return
1717

18-
# Get all cookies
1918
all_cookies = list(self.session.cookies)
20-
21-
# Clear all cookies
2219
self.session.cookies.clear()
2320

2421
# Add back only the most recent cookie for each name
@@ -32,7 +29,7 @@ def _get_csrf_token(self):
3229
for cookie in self.session.cookies:
3330
if cookie.name == 'csrftoken':
3431
return cookie.value
35-
return "iIf1V3zVGiU5F0neqw63pFm0YlFtk9i531xUBoQe0hZ06pmDGPZ0uJW6vyhr8GEH"
32+
return ''
3633

3734
def get_question_data(self, question_identifier: str) -> Dict[str, Any]:
3835
"""Get question details using GraphQL
@@ -83,28 +80,58 @@ def get_question_data(self, question_identifier: str) -> Dict[str, Any]:
8380

8481
def submit_solution(self, title_slug: str, code: str, lang: str = "python3") -> Dict[str, Any]:
8582
"""Submit a solution to LeetCode"""
83+
try:
84+
self._clean_session_cookies()
8685

87-
# First get the question ID
88-
question_data = self.get_question_data(title_slug)
89-
question_id = question_data['data']['question']['questionId']
86+
# Get question ID
87+
question_data = self.get_question_data(title_slug)
88+
question_data = question_data.get('data', {}).get('question')
89+
if not question_data:
90+
return {"success": False, "error": "Question data not found"}
9091

91-
submit_url = f"{self.BASE_URL}/problems/{title_slug}/submit/"
92+
question_id = question_data['questionId']
93+
submit_url = f"{self.BASE_URL}/problems/{title_slug}/submit/"
9294

93-
data = {
94-
"lang": lang,
95-
"question_id": question_id,
96-
"typed_code": code
97-
}
95+
csrf_token = self._get_csrf_token()
96+
typer.echo(f"Using CSRF token: {csrf_token}")
9897

99-
response = self.session.post(submit_url, json=data)
100-
if response.status_code != 200:
101-
return {"success": False, "error": f"Submission failed: {response.status_code}"}
98+
headers = {
99+
'referer': f"{self.BASE_URL}/problems/{title_slug}/",
100+
'content-type': 'application/json',
101+
'x-csrftoken': csrf_token,
102+
'x-requested-with': 'XMLHttpRequest',
103+
'origin': self.BASE_URL
104+
}
102105

103-
submission_id = response.json().get('submission_id')
104-
if not submission_id:
105-
return {"success": False, "error": "No submission ID received"}
106+
data = {
107+
"lang": lang,
108+
"question_id": question_id,
109+
"typed_code": code
110+
}
111+
112+
typer.echo(f"Sending request to {submit_url}")
113+
response = self.session.post(submit_url, json=data, headers=headers)
114+
115+
if response.status_code != 200:
116+
typer.echo(f"Error response: {response.text}", err=True)
117+
return {"success": False, "error": f"Submission failed with status {response.status_code}"}
118+
119+
try:
120+
result_data = response.json()
121+
submission_id = result_data.get('submission_id')
122+
if submission_id:
123+
typer.echo(f"Got submission ID: {submission_id}")
124+
return self.get_submission_result(submission_id)
125+
else:
126+
typer.echo("No submission ID in response", err=True)
127+
return {"success": False, "error": "No submission ID received"}
128+
except ValueError as e:
129+
typer.echo(f"Failed to parse response: {response.text}", err=True)
130+
return {"success": False, "error": f"Failed to parse response: {str(e)}"}
106131

107-
return self.get_submission_result(submission_id)
132+
except Exception as e:
133+
typer.echo(f"Submission error: {str(e)}", err=True)
134+
return {"success": False, "error": f"Submission error: {str(e)}"}
108135

109136
def test_solution(self, title_slug: str, code: str, lang: str = "python3", full: bool = False) -> Dict[str, Any]:
110137
"""Test a solution with LeetCode test cases"""
@@ -157,7 +184,7 @@ def test_solution(self, title_slug: str, code: str, lang: str = "python3", full:
157184
submission_id = result_data.get(sid_key)
158185
if submission_id:
159186
typer.echo(f"Got submission ID: {submission_id}")
160-
return self.get_result(submission_id)
187+
return self.get_test_result(submission_id)
161188
else:
162189
typer.echo("No submission ID in response", err=True)
163190
return {"success": False, "error": "No submission ID received"}
@@ -177,7 +204,7 @@ def _format_output(self, output) -> str:
177204
return output.strip('[]"')
178205
return str(output)
179206

180-
def get_result(self, submission_id: str, timeout: int = 30) -> Dict[str, Any]:
207+
def get_test_result(self, submission_id: str, timeout: int = 30) -> Dict[str, Any]:
181208
"""Poll for results with timeout"""
182209
url = f"{self.BASE_URL}/submissions/detail/{submission_id}/check/"
183210
typer.echo(f"Polling for results at {url}")
@@ -216,7 +243,7 @@ def get_submission_result(self, submission_id: str) -> Dict[str, Any]:
216243
"""Poll for submission results"""
217244
check_url = f"{self.BASE_URL}/submissions/detail/{submission_id}/check/"
218245

219-
for _ in range(20): # Try for 20 seconds
246+
for _ in range(20): #
220247
response = self.session.get(check_url)
221248
if response.status_code != 200:
222249
return {"success": False, "error": f"Failed to get results: {response.status_code}"}
@@ -234,50 +261,4 @@ def get_submission_result(self, submission_id: str) -> Dict[str, Any]:
234261

235262
time.sleep(1)
236263

237-
return {"success": False, "error": "Timeout waiting for results"}
238-
239-
def get_test_result(self, interpret_id: str) -> Dict[str, Any]:
240-
"""Poll for test results"""
241-
check_url = f"{self.BASE_URL}/submissions/detail/{interpret_id}/check/"
242-
243-
headers = {
244-
'Accept': 'application/json',
245-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
246-
}
247-
248-
max_attempts = 10
249-
for attempt in range(max_attempts):
250-
try:
251-
response = self.session.get(check_url, headers=headers)
252-
if response.status_code != 200:
253-
return {"success": False, "error": f"Failed to get results: {response.status_code}"}
254-
255-
result = response.json()
256-
257-
# Check for compilation errors first
258-
if result.get('status_msg') == 'Compile Error':
259-
return {
260-
"success": False,
261-
"error": f"Compilation Error:\n{result.get('compile_error', 'Unknown compilation error')}"
262-
}
263-
264-
if result.get('state') == 'SUCCESS':
265-
status_msg = result.get('status_msg', 'Unknown')
266-
return {
267-
"success": True,
268-
"status": status_msg,
269-
"input": result.get('input', 'N/A'),
270-
"output": result.get('code_output', 'N/A').strip('[]"'),
271-
"expected": result.get('expected_output', 'N/A').strip('[]"'),
272-
"runtime": result.get('status_runtime', 'N/A'),
273-
"memory": result.get('memory', 'N/A'),
274-
}
275-
276-
# If not success yet, wait before next attempt
277-
if attempt < max_attempts - 1:
278-
time.sleep(2)
279-
280-
except Exception as e:
281-
return {"success": False, "error": f"Error checking results: {str(e)}"}
282-
283-
return {"success": False, "error": "Timeout waiting for test results"}
264+
return {"success": False, "error": "Timeout waiting for results"}

0 commit comments

Comments
 (0)