Skip to content
Snippets Groups Projects
Commit 7f3b1b67 authored by Martin Mareš's avatar Martin Mareš
Browse files

Mail: DSN tokeny posíláme i v registraci

parent 11eae74c
No related branches found
No related tags found
No related merge requests found
...@@ -29,7 +29,6 @@ def send_email(send_to: str, full_name: str, subject: str, body: str, dsn_token: ...@@ -29,7 +29,6 @@ def send_email(send_to: str, full_name: str, subject: str, body: str, dsn_token:
env_from = mail_from env_from = mail_from
if dsn_token is not None: if dsn_token is not None:
logger.info(f'Mail: DSN token {dsn_token}')
env_from = env_from.replace('@', f'+{dsn_token}@') env_from = env_from.replace('@', f'+{dsn_token}@')
msg = email.message.EmailMessage() msg = email.message.EmailMessage()
...@@ -79,22 +78,25 @@ def send_email(send_to: str, full_name: str, subject: str, body: str, dsn_token: ...@@ -79,22 +78,25 @@ def send_email(send_to: str, full_name: str, subject: str, body: str, dsn_token:
return True return True
def dsn_token_signature(raw_token: str, user: db.User, secret: str) -> str: def dsn_token_signature(raw_token: str, email: str, secret: str) -> str:
body = f'{raw_token}-{user.email}' body = f'{raw_token}-{email}'
raw_sig = hmac.digest(secret.encode('utf-8'), body.encode('utf-8'), 'sha1')[:5] raw_sig = hmac.digest(secret.encode('utf-8'), body.encode('utf-8'), 'sha1')[:5]
return base64.b32encode(raw_sig).decode('utf-8').lower() return base64.b32encode(raw_sig).decode('utf-8').lower()
def gen_dsn_token(user: db.User) -> Optional[str]: def gen_dsn_token(user: db.User, rr: Optional[db.RegRequest] = None) -> Optional[str]:
# E-maily posílané na adresy našich uživatelů mají obálkového odesilatele # E-maily posílané na adresy našich uživatelů mají obálkového odesilatele
# parametrizovaného tokenem, který používáme při párování Delivery Status # parametrizovaného tokenem, který používáme při párování Delivery Status
# Notifications s účtem. # Notifications s účtem.
# #
# Tokeny obsahují user_id, čas a kryptografický podpis (HMAC), který # Podobné tokeny mají e-maily z registrace (nové účty, resety hesla, změny
# adres). Ty jsou vázané na reg_requests v DB.
#
# Tokeny obsahují user_id nebo reg_id, čas a kryptografický podpis (HMAC), který
# podepisuje i e-mailovou adresu účtu, abychom se nenechali zmást ani falešnou # podepisuje i e-mailovou adresu účtu, abychom se nenechali zmást ani falešnou
# nedoručenkou, ani opožděnou nedoručenkou, zatímco si uživatel změnil adresu. # nedoručenkou, ani opožděnou nedoručenkou, zatímco si uživatel změnil adresu.
# #
# Formát tokenu: <user_id>-<unix_timestamp>-<podpis> # Formát tokenu: (<user_id>|r<reg_id>)-<unix_timestamp>-<podpis>
secret = getattr(config, 'MAIL_TOKEN_SECRET', None) secret = getattr(config, 'MAIL_TOKEN_SECRET', None)
if secret is None: if secret is None:
...@@ -102,22 +104,30 @@ def gen_dsn_token(user: db.User) -> Optional[str]: ...@@ -102,22 +104,30 @@ def gen_dsn_token(user: db.User) -> Optional[str]:
now = int(mo.now.timestamp()) now = int(mo.now.timestamp())
assert now > 0 assert now > 0
raw_token = f'{user.user_id}-{now}'
sig = dsn_token_signature(raw_token, user, secret) if rr is not None:
ident = f'r{rr.reg_id}'
email = rr.email
else:
ident = str(user.user_id)
email = user.email
raw_token = f'{ident}-{now}'
sig = dsn_token_signature(raw_token, email, secret)
return f'{raw_token}-{sig}' return f'{raw_token}-{sig}'
def validate_dsn_token(token: str) -> Tuple[db.User, datetime]: def validate_dsn_token(token: str) -> Tuple[Optional[db.User], Optional[db.RegRequest], datetime]:
secret = getattr(config, 'MAIL_TOKEN_SECRET', None) secret = getattr(config, 'MAIL_TOKEN_SECRET', None)
if secret is None: if secret is None:
raise ValueError("MAIL_TOKEN_SECRET nenastaven") raise ValueError("MAIL_TOKEN_SECRET nenastaven")
fields = token.split('-') fields = token.split('-')
if (len(fields) != 3 or if (len(fields) != 3 or
not re.match(r'[1-9]\d{0,9}', fields[0]) or not re.match(r'r?[1-9]\d{0,9}', fields[0]) or
not re.match(r'[1-9]\d{0,9}', fields[1])): not re.match(r'[1-9]\d{0,9}', fields[1])):
raise ValueError("Chybná syntaxe") raise ValueError("Chybná syntaxe")
user_id, timestamp, given_sig = int(fields[0]), int(fields[1]), fields[2] ident, timestamp, given_sig = fields[0], int(fields[1]), fields[2]
when = datetime.fromtimestamp(timestamp).astimezone(dateutil.tz.UTC) when = datetime.fromtimestamp(timestamp).astimezone(dateutil.tz.UTC)
age = mo.now - when age = mo.now - when
...@@ -127,19 +137,35 @@ def validate_dsn_token(token: str) -> Tuple[db.User, datetime]: ...@@ -127,19 +137,35 @@ def validate_dsn_token(token: str) -> Tuple[db.User, datetime]:
raise ValueError("Token z budoucnosti") raise ValueError("Token z budoucnosti")
sess = db.get_session() sess = db.get_session()
if ident.startswith('r'):
reg_id = int(ident[1:])
user = None
rr = sess.query(db.RegRequest).get(reg_id)
if rr is None:
raise ValueError("Registrace neexistuje")
email = rr.email
else:
user_id = int(ident)
user = sess.query(db.User).get(user_id) user = sess.query(db.User).get(user_id)
if user is None: if user is None:
raise ValueError("Uživatel neexistuje") raise ValueError("Uživatel neexistuje")
rr = None
email = user.email
raw_token = f'{user_id}-{timestamp}' raw_token = f'{ident}-{timestamp}'
correct_sig = dsn_token_signature(raw_token, user, secret) correct_sig = dsn_token_signature(raw_token, email, secret)
if given_sig != correct_sig: if given_sig != correct_sig:
raise ValueError("Nesouhlasí podpis") raise ValueError("Nesouhlasí podpis")
return user, when return user, rr, when
def send_user_email(user: db.User, subject: str, body: str, add_footer: bool = False, override_email: Optional[str] = None, send_dsn_token: bool = True) -> bool: def send_user_email(user: db.User,
subject: str,
body: str,
add_footer: bool = False,
override_email: Optional[str] = None,
rr: Optional[db.RegRequest] = None) -> bool:
if override_email: if override_email:
email = override_email email = override_email
elif user.user_id == 0: elif user.user_id == 0:
...@@ -149,12 +175,11 @@ def send_user_email(user: db.User, subject: str, body: str, add_footer: bool = F ...@@ -149,12 +175,11 @@ def send_user_email(user: db.User, subject: str, body: str, add_footer: bool = F
else: else:
email = user.email email = user.email
logger.info(f'Mail: "{subject}" -> {email} (#{user.user_id})') dsn_token = gen_dsn_token(user, rr)
if send_dsn_token: user_suffix = f' (#{user.user_id})' if user.user_id is not None else ""
dsn_token = gen_dsn_token(user) dsn_suffix = f', DSN token {dsn_token}' if dsn_token else ""
else: logger.info(f'Mail: "{subject}" -> {email}{user_suffix}{dsn_suffix}')
dsn_token = None
if add_footer: if add_footer:
body += "\n" + ("=" * 76) + "\n" body += "\n" + ("=" * 76) + "\n"
...@@ -213,7 +238,7 @@ def send_password_reset_email(user: db.User, token: str) -> bool: ...@@ -213,7 +238,7 @@ def send_password_reset_email(user: db.User, token: str) -> bool:
'''.format(confirm_url('p', token)))) '''.format(confirm_url('p', token))))
def send_confirm_create_email(user: db.User, token: str) -> bool: def send_confirm_create_email(user: db.User, rr: db.RegRequest) -> bool:
return send_user_email(user, 'Založení účtu', textwrap.dedent('''\ return send_user_email(user, 'Založení účtu', textwrap.dedent('''\
Někdo požádal o založení účtu s touto e-mailovou adresou v Odevzdávacím Někdo požádal o založení účtu s touto e-mailovou adresou v Odevzdávacím
systému Matematické olympiády. systému Matematické olympiády.
...@@ -223,10 +248,10 @@ def send_confirm_create_email(user: db.User, token: str) -> bool: ...@@ -223,10 +248,10 @@ def send_confirm_create_email(user: db.User, token: str) -> bool:
{} {}
Váš OSMO Váš OSMO
'''.format(confirm_url('r', token))), send_dsn_token=False) '''.format(confirm_url('r', rr.email_token))), rr=rr)
def send_confirm_change_email(user: db.User, token: str, new_email: str) -> bool: def send_confirm_change_email(user: db.User, rr: db.RegRequest) -> bool:
return send_user_email(user, 'Změna e-mailové adresy', textwrap.dedent('''\ return send_user_email(user, 'Změna e-mailové adresy', textwrap.dedent('''\
Někdo požádal o nastavení e-mailové adresy k účtu v Odevzdávacím Někdo požádal o nastavení e-mailové adresy k účtu v Odevzdávacím
systému Matematické olympiády na tuto adresu. systému Matematické olympiády na tuto adresu.
...@@ -236,7 +261,7 @@ def send_confirm_change_email(user: db.User, token: str, new_email: str) -> bool ...@@ -236,7 +261,7 @@ def send_confirm_change_email(user: db.User, token: str, new_email: str) -> bool
{} {}
Váš OSMO Váš OSMO
'''.format(confirm_url('e', token))), override_email=new_email) '''.format(confirm_url('e', rr.email_token))), override_email=rr.email, rr=rr)
def send_join_notify_email(dest: db.User, who: db.User, contest: db.Contest) -> bool: def send_join_notify_email(dest: db.User, who: db.User, contest: db.Contest) -> bool:
......
...@@ -208,7 +208,7 @@ def user_settings_personal(): ...@@ -208,7 +208,7 @@ def user_settings_personal():
sess.commit() sess.commit()
app.logger.info(f'Settings: Požadavek na změnu e-mailu uživatele #{user.user_id}') app.logger.info(f'Settings: Požadavek na změnu e-mailu uživatele #{user.user_id}')
flash('Odeslán e-mail s odkazem na potvrzení nové adresy.', 'success') flash('Odeslán e-mail s odkazem na potvrzení nové adresy.', 'success')
mo.email.send_confirm_change_email(user, rr.email_token, new_email=rr.email) mo.email.send_confirm_change_email(user, rr)
else: else:
app.logger.info('Settings: Rate limit') app.logger.info('Settings: Rate limit')
flash('Příliš mnoho požadavků na změny e-mailu. Počkejte prosím chvíli a zkuste to znovu.', 'danger') flash('Příliš mnoho požadavků na změny e-mailu. Počkejte prosím chvíli a zkuste to znovu.', 'danger')
...@@ -278,6 +278,7 @@ class Reg1: ...@@ -278,6 +278,7 @@ class Reg1:
create_time: datetime.datetime create_time: datetime.datetime
seed: str seed: str
status: RegStatus status: RegStatus
rr: db.RegRequest
email_token: str email_token: str
x: int x: int
y: int y: int
...@@ -361,6 +362,7 @@ class Reg1: ...@@ -361,6 +362,7 @@ class Reg1:
app.logger.info('Reg1: Rate limit') app.logger.info('Reg1: Rate limit')
return False return False
self.rr = rr
self.email_token = rr.email_token self.email_token = rr.email_token
rr.email = email rr.email = email
rr.captcha_token = self.seed rr.captcha_token = self.seed
...@@ -406,7 +408,7 @@ def create_acct(): ...@@ -406,7 +408,7 @@ def create_acct():
app.logger.debug(f'Reg1: E-mailový token {reg1.email_token}') app.logger.debug(f'Reg1: E-mailový token {reg1.email_token}')
flash('Odeslán e-mail s odkazem na založení účtu.', 'success') flash('Odeslán e-mail s odkazem na založení účtu.', 'success')
user = db.User(email=form.email.data, first_name='Nový', last_name='Uživatel') user = db.User(email=form.email.data, first_name='Nový', last_name='Uživatel')
mo.email.send_confirm_create_email(user, reg1.email_token) mo.email.send_confirm_create_email(user, reg1.rr)
return redirect(url_for('confirm_reg')) return redirect(url_for('confirm_reg'))
form.captcha.description = reg1.captcha_task() form.captcha.description = reg1.captcha_task()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment