Skip to content

Commit f35ac27

Browse files
authored
Add recaptcha protection for password resets (#12)
* Add recaptcha protection for password resets * move recaptcha to init * bool conv
1 parent c6ce2c1 commit f35ac27

File tree

4 files changed

+55
-0
lines changed

4 files changed

+55
-0
lines changed

app/adapters/recaptcha.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import logging
2+
3+
import httpx
4+
5+
from app import settings
6+
7+
recaptcha_http_client = httpx.AsyncClient(
8+
base_url="https://www.google.com/recaptcha",
9+
)
10+
11+
12+
async def verify_recaptcha(
13+
*,
14+
recaptcha_token: str,
15+
client_ip_address: str,
16+
) -> bool:
17+
try:
18+
response = await recaptcha_http_client.post(
19+
"/api/siteverify",
20+
data={
21+
"secret": settings.RECAPTCHA_SECRET_KEY,
22+
"response": recaptcha_token,
23+
"remoteip": client_ip_address, # https://stackoverflow.com/a/51920956
24+
},
25+
)
26+
response.raise_for_status()
27+
response_data = response.json()
28+
if not isinstance(response_data, dict):
29+
raise ValueError("Invalid response from recaptcha")
30+
return response_data.get("success", False) is True
31+
32+
except Exception:
33+
logging.exception(
34+
"Failed to verify recaptcha",
35+
extra={
36+
"recaptcha_token": recaptcha_token,
37+
"client_ip_address": client_ip_address,
38+
},
39+
)
40+
return False

app/api/public/authentication.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ async def logout(
107107

108108
class InitializePasswordResetRequest(BaseModel):
109109
username: str
110+
recaptcha_token: str
110111

111112

112113
@router.post("/public/api/v1/init-password-reset")
@@ -117,6 +118,7 @@ async def initialize_password_reset(
117118
) -> Response:
118119
response = await authentication.initialize_password_reset(
119120
username=args.username,
121+
recaptcha_token=args.recaptcha_token,
120122
client_ip_address=client_ip_address,
121123
client_user_agent=client_user_agent,
122124
)

app/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ def read_bool(s: str) -> bool:
3939
MAILGUN_BASE_URL = os.environ["MAILGUN_BASE_URL"]
4040
MAILGUN_DOMAIN_NAME = os.environ["MAILGUN_DOMAIN_NAME"]
4141
MAILGUN_API_KEY = os.environ["MAILGUN_API_KEY"]
42+
43+
RECAPTCHA_SECRET_KEY = os.environ["RECAPTCHA_SECRET_KEY"]

app/usecases/authentication.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from app import security
77
from app.adapters import mailgun
8+
from app.adapters import recaptcha
89
from app.common_types import UserPrivileges
910
from app.errors import Error
1011
from app.errors import ErrorCode
@@ -115,9 +116,19 @@ async def logout(
115116
async def initialize_password_reset(
116117
*,
117118
username: str,
119+
recaptcha_token: str,
118120
client_ip_address: str,
119121
client_user_agent: str,
120122
) -> None | Error:
123+
if not await recaptcha.verify_recaptcha(
124+
recaptcha_token=recaptcha_token,
125+
client_ip_address=client_ip_address,
126+
):
127+
return Error(
128+
error_code=ErrorCode.INCORRECT_CREDENTIALS,
129+
user_feedback="Invalid reCAPTCHA token.",
130+
)
131+
121132
user = await users.fetch_one_by_username(username)
122133
if user is None:
123134
return Error(

0 commit comments

Comments
 (0)