Coverage for gws-app/gws/plugin/auth_mfa/email/__init__.py: 0%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1"""Multi-factor authenticator that sends TOTPs per email. 

2 

3The user is required to have an ``email`` attribute. 

4User secret is dynamically generated each time new TOTP is created. 

5 

6""" 

7 

8from typing import Optional, cast 

9 

10import gws 

11import gws.base.auth 

12import gws.plugin.email_helper 

13import gws.lib.otp 

14 

15gws.ext.new.authMultiFactorAdapter('email') 

16 

17 

18class Config(gws.base.auth.mfa.Config): 

19 """Email multi-factor authenticator configuration.""" 

20 

21 templates: Optional[list[gws.ext.config.template]] 

22 """Email templates.""" 

23 

24 

25class Object(gws.base.auth.mfa.Object): 

26 templates: list[gws.Template] 

27 

28 def configure(self): 

29 self.templates = self.create_children(gws.ext.object.template, self.cfg('templates')) 

30 

31 def start(self, user): 

32 if not user.email: 

33 gws.log.warning(f'email: cannot start, {user.uid=}: no email') 

34 return 

35 mfa = super().start(user) 

36 self.generate_and_send(mfa) 

37 return mfa 

38 

39 def verify(self, mfa, payload): 

40 ok = self.check_totp(mfa, payload.get('code')) 

41 return self.verify_attempt(mfa, ok) 

42 

43 ## 

44 

45 def generate_and_send(self, mfa: gws.AuthMultiFactorTransaction): 

46 # NB regenerate secret on each attempt 

47 mfa.secret = gws.lib.otp.random_secret() 

48 

49 args = { 

50 'user': mfa.user, 

51 'otp': self.generate_totp(mfa), 

52 } 

53 message = gws.plugin.email_helper.Message( 

54 subject=self.render_template('email.subject', args), 

55 mailTo=mfa.user.email, 

56 text=self.render_template('email.body', args, mime='text/plain'), 

57 html=self.render_template('email.body', args, mime='text/html'), 

58 ) 

59 

60 email_helper = cast(gws.plugin.email_helper.Object, self.root.app.helper('email')) 

61 email_helper.send_mail(message) 

62 

63 def render_template(self, subject, args, mime=None): 

64 tpl = self.root.app.templateMgr.find_template(subject, where=[self], mime=mime) 

65 if tpl: 

66 res = tpl.render(gws.TemplateRenderInput(args=args)) 

67 return res.content 

68 return ''