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
« 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.
3The user is required to have an ``email`` attribute.
4User secret is dynamically generated each time new TOTP is created.
6"""
8from typing import Optional, cast
10import gws
11import gws.base.auth
12import gws.plugin.email_helper
13import gws.lib.otp
15gws.ext.new.authMultiFactorAdapter('email')
18class Config(gws.base.auth.mfa.Config):
19 """Email multi-factor authenticator configuration."""
21 templates: Optional[list[gws.ext.config.template]]
22 """Email templates."""
25class Object(gws.base.auth.mfa.Object):
26 templates: list[gws.Template]
28 def configure(self):
29 self.templates = self.create_children(gws.ext.object.template, self.cfg('templates'))
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
39 def verify(self, mfa, payload):
40 ok = self.check_totp(mfa, payload.get('code'))
41 return self.verify_attempt(mfa, ok)
43 ##
45 def generate_and_send(self, mfa: gws.AuthMultiFactorTransaction):
46 # NB regenerate secret on each attempt
47 mfa.secret = gws.lib.otp.random_secret()
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 )
60 email_helper = cast(gws.plugin.email_helper.Object, self.root.app.helper('email'))
61 email_helper.send_mail(message)
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 ''