1919from typing import TYPE_CHECKING , Optional , Tuple
2020from urllib .parse import urlparse
2121
22- from pydantic import BaseModel , StrictBool , StrictStr , constr
22+ from pydantic import BaseModel , StrictBool , StrictStr , constr , validator
2323
2424from twisted .web .server import Request
2525
5858logger = logging .getLogger (__name__ )
5959
6060
61+ class EmailPasswordRequestBody (BaseModel ):
62+ if TYPE_CHECKING :
63+ client_secret : str
64+ else :
65+ # See also assert_valid_client_secret()
66+ client_secret : constr (
67+ regex = "[0-9a-zA-Z.=_-]" , min_length = 0 , max_length = 255 # noqa: F722
68+ )
69+ email : str
70+ id_access_token : Optional [str ]
71+ id_server : Optional [str ]
72+ next_link : Optional [str ]
73+ send_attempt : int
74+
75+ # Canonicalise the email address. The addresses are all stored canonicalised
76+ # in the database. This allows the user to reset his password without having to
77+ # know the exact spelling (eg. upper and lower case) of address in the database.
78+ # Without this, an email stored in the database as "foo@bar.com" would cause
79+ # user requests for "FOO@bar.com" to raise a Not Found error.
80+ _email_validator = validator ("email" , allow_reuse = True )(validate_email )
81+
82+
6183class EmailPasswordRequestTokenRestServlet (RestServlet ):
6284 PATTERNS = client_patterns ("/account/password/email/requestToken$" )
6385
@@ -88,40 +110,24 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
88110 Codes .NOT_FOUND ,
89111 )
90112
91- body = parse_json_object_from_request (request )
92-
93- assert_params_in_dict (body , ["client_secret" , "email" , "send_attempt" ])
94-
95- # Extract params from body
96- client_secret = body ["client_secret" ]
97- assert_valid_client_secret (client_secret )
98-
99- # Canonicalise the email address. The addresses are all stored canonicalised
100- # in the database. This allows the user to reset his password without having to
101- # know the exact spelling (eg. upper and lower case) of address in the database.
102- # Stored in the database "foo@bar.com"
103- # User requests with "FOO@bar.com" would raise a Not Found error
104- try :
105- email = validate_email (body ["email" ])
106- except ValueError as e :
107- raise SynapseError (400 , str (e ))
108- send_attempt = body ["send_attempt" ]
109- next_link = body .get ("next_link" ) # Optional param
113+ body = parse_and_validate_json_object_from_request (
114+ request , EmailPasswordRequestBody
115+ )
110116
111- if next_link :
117+ if body . next_link :
112118 # Raise if the provided next_link value isn't valid
113- assert_valid_next_link (self .hs , next_link )
119+ assert_valid_next_link (self .hs , body . next_link )
114120
115121 await self .identity_handler .ratelimit_request_token_requests (
116- request , "email" , email
122+ request , "email" , body . email
117123 )
118124
119125 # The email will be sent to the stored address.
120126 # This avoids a potential account hijack by requesting a password reset to
121127 # an email address which is controlled by the attacker but which, after
122128 # canonicalisation, matches the one in our database.
123129 existing_user_id = await self .hs .get_datastores ().main .get_user_id_by_threepid (
124- "email" , email
130+ "email" , body . email
125131 )
126132
127133 if existing_user_id is None :
@@ -141,26 +147,26 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
141147 # Have the configured identity server handle the request
142148 ret = await self .identity_handler .requestEmailToken (
143149 self .hs .config .registration .account_threepid_delegate_email ,
144- email ,
145- client_secret ,
146- send_attempt ,
147- next_link ,
150+ body . email ,
151+ body . client_secret ,
152+ body . send_attempt ,
153+ body . next_link ,
148154 )
149155 else :
150156 # Send password reset emails from Synapse
151157 sid = await self .identity_handler .send_threepid_validation (
152- email ,
153- client_secret ,
154- send_attempt ,
158+ body . email ,
159+ body . client_secret ,
160+ body . send_attempt ,
155161 self .mailer .send_password_reset_mail ,
156- next_link ,
162+ body . next_link ,
157163 )
158164
159165 # Wrap the session id in a JSON object
160166 ret = {"sid" : sid }
161167
162168 threepid_send_requests .labels (type = "email" , reason = "password_reset" ).observe (
163- send_attempt
169+ body . send_attempt
164170 )
165171
166172 return 200 , ret
0 commit comments