@ -15,19 +15,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import re
from six . moves import http_client
import jinja2
from twisted . internet import defer
from synapse . api . constants import LoginType
from synapse . api . errors import Codes , SynapseError
from synapse . api . errors import Codes , SynapseError , ThreepidValidationError
from synapse . http . server import finish_request
from synapse . http . servlet import (
RestServlet ,
assert_params_in_dict ,
parse_json_object_from_request ,
parse_string ,
)
from synapse . util . msisdn import phone_number_to_msisdn
from synapse . util . stringutils import random_string
from synapse . util . threepids import check_3pid_allowed
from . _base import client_patterns , interactive_auth_handler
@ -41,17 +47,42 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
def __init__ ( self , hs ) :
super ( EmailPasswordRequestTokenRestServlet , self ) . __init__ ( )
self . hs = hs
self . datastore = hs . get_datastore ( )
self . config = hs . config
self . identity_handler = hs . get_handlers ( ) . identity_handler
if self . config . email_password_reset_behaviour == " local " :
from synapse . push . mailer import Mailer , load_jinja2_templates
templates = load_jinja2_templates (
config = hs . config ,
template_html_name = hs . config . email_password_reset_template_html ,
template_text_name = hs . config . email_password_reset_template_text ,
)
self . mailer = Mailer (
hs = self . hs ,
app_name = self . config . email_app_name ,
template_html = templates [ 0 ] ,
template_text = templates [ 1 ] ,
)
@defer . inlineCallbacks
def on_POST ( self , request ) :
if self . config . email_password_reset_behaviour == " off " :
raise SynapseError ( 400 , " Password resets have been disabled on this server " )
body = parse_json_object_from_request ( request )
assert_params_in_dict ( body , [
' id_server ' , ' client_secret ' , ' email ' , ' send_attempt '
' client_secret ' , ' email ' , ' send_attempt '
] )
if not check_3pid_allowed ( self . hs , " email " , body [ ' email ' ] ) :
# Extract params from body
client_secret = body [ " client_secret " ]
email = body [ " email " ]
send_attempt = body [ " send_attempt " ]
next_link = body . get ( " next_link " ) # Optional param
if not check_3pid_allowed ( self . hs , " email " , email ) :
raise SynapseError (
403 ,
" Your email domain is not authorized on this server " ,
@ -59,15 +90,100 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
)
existingUid = yield self . hs . get_datastore ( ) . get_user_id_by_threepid (
' email ' , body [ ' email ' ]
' email ' , email ,
)
if existingUid is None :
raise SynapseError ( 400 , " Email not found " , Codes . THREEPID_NOT_FOUND )
ret = yield self . identity_handler . requestEmailToken ( * * body )
if self . config . email_password_reset_behaviour == " remote " :
if ' id_server ' not in body :
raise SynapseError ( 400 , " Missing ' id_server ' param in body " )
# Have the identity server handle the password reset flow
ret = yield self . identity_handler . requestEmailToken (
body [ " id_server " ] , email , client_secret , send_attempt , next_link ,
)
else :
# Send password reset emails from Synapse
sid = yield self . send_password_reset (
email , client_secret , send_attempt , next_link ,
)
# Wrap the session id in a JSON object
ret = { " sid " : sid }
defer . returnValue ( ( 200 , ret ) )
@defer . inlineCallbacks
def send_password_reset (
self ,
email ,
client_secret ,
send_attempt ,
next_link = None ,
) :
""" Send a password reset email
Args :
email ( str ) : The user ' s email address
client_secret ( str ) : The provided client secret
send_attempt ( int ) : Which send attempt this is
Returns :
The new session_id upon success
Raises :
SynapseError is an error occurred when sending the email
"""
# Check that this email/client_secret/send_attempt combo is new or
# greater than what we've seen previously
session = yield self . datastore . get_threepid_validation_session (
" email " , client_secret , address = email , validated = False ,
)
# Check to see if a session already exists and that it is not yet
# marked as validated
if session and session . get ( " validated_at " ) is None :
session_id = session [ ' session_id ' ]
last_send_attempt = session [ ' last_send_attempt ' ]
# Check that the send_attempt is higher than previous attempts
if send_attempt < = last_send_attempt :
# If not, just return a success without sending an email
defer . returnValue ( session_id )
else :
# An non-validated session does not exist yet.
# Generate a session id
session_id = random_string ( 16 )
# Generate a new validation token
token = random_string ( 32 )
# Send the mail with the link containing the token, client_secret
# and session_id
try :
yield self . mailer . send_password_reset_mail (
email , token , client_secret , session_id ,
)
except Exception :
logger . exception (
" Error sending a password reset email to %s " , email ,
)
raise SynapseError (
500 , " An error was encountered when sending the password reset email "
)
token_expires = ( self . hs . clock . time_msec ( ) +
self . config . email_validation_token_lifetime )
yield self . datastore . start_or_continue_validation_session (
" email " , email , session_id , client_secret , send_attempt ,
next_link , token , token_expires ,
)
defer . returnValue ( session_id )
class MsisdnPasswordRequestTokenRestServlet ( RestServlet ) :
PATTERNS = client_patterns ( " /account/password/msisdn/requestToken$ " )
@ -80,6 +196,9 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
@defer . inlineCallbacks
def on_POST ( self , request ) :
if not self . config . email_password_reset_behaviour == " off " :
raise SynapseError ( 400 , " Password resets have been disabled on this server " )
body = parse_json_object_from_request ( request )
assert_params_in_dict ( body , [
@ -107,6 +226,118 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
defer . returnValue ( ( 200 , ret ) )
class PasswordResetSubmitTokenServlet ( RestServlet ) :
""" Handles 3PID validation token submission """
PATTERNS = [
re . compile ( " ^/_synapse/password_reset/(?P<medium>[^/]*)/submit_token/*$ " ) ,
]
def __init__ ( self , hs ) :
"""
Args :
hs ( synapse . server . HomeServer ) : server
"""
super ( PasswordResetSubmitTokenServlet , self ) . __init__ ( )
self . hs = hs
self . auth = hs . get_auth ( )
self . config = hs . config
self . clock = hs . get_clock ( )
self . datastore = hs . get_datastore ( )
@defer . inlineCallbacks
def on_GET ( self , request , medium ) :
if medium != " email " :
raise SynapseError (
400 ,
" This medium is currently not supported for password resets " ,
)
sid = parse_string ( request , " sid " )
client_secret = parse_string ( request , " client_secret " )
token = parse_string ( request , " token " )
# Attempt to validate a 3PID sesssion
try :
# Mark the session as valid
next_link = yield self . datastore . validate_threepid_session (
sid ,
client_secret ,
token ,
self . clock . time_msec ( ) ,
)
# Perform a 302 redirect if next_link is set
if next_link :
if next_link . startswith ( " file:/// " ) :
logger . warn (
" Not redirecting to next_link as it is a local file: address "
)
else :
request . setResponseCode ( 302 )
request . setHeader ( " Location " , next_link )
finish_request ( request )
defer . returnValue ( None )
# Otherwise show the success template
html = self . config . email_password_reset_success_html_content
request . setResponseCode ( 200 )
except ThreepidValidationError as e :
# Show a failure page with a reason
html = self . load_jinja2_template (
self . config . email_template_dir ,
self . config . email_password_reset_failure_template ,
template_vars = {
" failure_reason " : e . msg ,
}
)
request . setResponseCode ( e . code )
request . write ( html . encode ( ' utf-8 ' ) )
finish_request ( request )
defer . returnValue ( None )
def load_jinja2_template ( self , template_dir , template_filename , template_vars ) :
""" Loads a jinja2 template with variables to insert
Args :
template_dir ( str ) : The directory where templates are stored
template_filename ( str ) : The name of the template in the template_dir
template_vars ( Dict ) : Dictionary of keys in the template
alongside their values to insert
Returns :
str containing the contents of the rendered template
"""
loader = jinja2 . FileSystemLoader ( template_dir )
env = jinja2 . Environment ( loader = loader )
template = env . get_template ( template_filename )
return template . render ( * * template_vars )
@defer . inlineCallbacks
def on_POST ( self , request , medium ) :
if medium != " email " :
raise SynapseError (
400 ,
" This medium is currently not supported for password resets " ,
)
body = parse_json_object_from_request ( request )
assert_params_in_dict ( body , [
' sid ' , ' client_secret ' , ' token ' ,
] )
valid , _ = yield self . datastore . validate_threepid_validation_token (
body [ ' sid ' ] ,
body [ ' client_secret ' ] ,
body [ ' token ' ] ,
self . clock . time_msec ( ) ,
)
response_code = 200 if valid else 400
defer . returnValue ( ( response_code , { " success " : valid } ) )
class PasswordRestServlet ( RestServlet ) :
PATTERNS = client_patterns ( " /account/password$ " )
@ -144,6 +375,7 @@ class PasswordRestServlet(RestServlet):
result , params , _ = yield self . auth_handler . check_auth (
[ [ LoginType . EMAIL_IDENTITY ] , [ LoginType . MSISDN ] ] ,
body , self . hs . get_ip_from_request ( request ) ,
password_servlet = True ,
)
if LoginType . EMAIL_IDENTITY in result :
@ -417,6 +649,7 @@ class WhoamiRestServlet(RestServlet):
def register_servlets ( hs , http_server ) :
EmailPasswordRequestTokenRestServlet ( hs ) . register ( http_server )
MsisdnPasswordRequestTokenRestServlet ( hs ) . register ( http_server )
PasswordResetSubmitTokenServlet ( hs ) . register ( http_server )
PasswordRestServlet ( hs ) . register ( http_server )
DeactivateAccountRestServlet ( hs ) . register ( http_server )
EmailThreepidRequestTokenRestServlet ( hs ) . register ( http_server )