2-Factor Authentication QR Code for Google Authenticator

47 views
Skip to first unread message

Stefan Messmer

unread,
Jan 3, 2026, 7:40:01 AMJan 3
to py4web
I have implemented the infrastructure needed to provide time based 2-Factor Authentication. To make this feature easy usable, I should place a QR-code on the profile page. The QR-code can be provided either by:
  • data-uri or
  • inline svg
What's the easiest way to display such a code on the profile page?

Best regards and many thanks
Stefan

Christian Varas

unread,
Jan 3, 2026, 8:17:11 AMJan 3
to Stefan Messmer, py4web
Hi, there is no a easy way to do this, you will have too look  some auth plugin inside py4web folder and do a plugin for this. I did some time ago a passkeys/yubikey authentication plugin by looking those auth plugins, but I still don’t commit them, hopefully I’ll do this month.

greetings.

--
You received this message because you are subscribed to the Google Groups "py4web" group.
To unsubscribe from this group and stop receiving emails from it, send an email to py4web+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/py4web/46c0f927-c169-4b1f-b305-2eb20d19ab37n%40googlegroups.com.

Massimo DiPierro

unread,
Jan 3, 2026, 11:42:27 AMJan 3
to py4web
One way to do it is adding an auth_user readonly field with a represent=lambda url: yaml.helpers.IMG(_src=url2qr(url)) or maybe a custom widget.
I can help with this if you post what you have.

Stefan Messmer

unread,
Jan 3, 2026, 3:30:43 PMJan 3
to py4web
Massimo's suggestion works if the typo is corrected (yatl.helpers.IMG(.....)) ;-). I will share the code as soon as everything is working and has been tested.

Best regards
Stefan

Massimo DiPierro

unread,
Jan 3, 2026, 7:09:40 PMJan 3
to py4web
:-)

Stefan Messmer

unread,
Jan 4, 2026, 7:25:45 AMJan 4
to py4web
Here is the code that works for me (at the moment, modified common.py from Scaffold app). It is surly  not perfect, but it has the key features. I added the extra_fields tfa_required, secret and qr_code to the auth_user db. The secret is generated on the first login and is no writable. The field qr_code is computed and the field tfa_required is used to keep track of TFA required status. If the user leaves it empty, it is set to "F", meaning no TFA is required. If the user enters a 6 character code, it is set to "T" (TFA required). Unfortunately I cannot verify the correct code at the moment (needs a validation function). If anybody has an idea how to implement this feature, I would appreciate very much. The codes are generated with pyotp and the QR-code is generated with segno. I have tested the TFA with Authy and Google Authenticator. I

# #######################################################

# Instantiate the object and actions that handle auth

# #######################################################

def tfa_is_activated(user, request):

    try:

        # get thr user record 

        tfa_required=False

        for row in db(db.auth_user.email == user.get('email')).select():

            tfa_required = True if (row.tfa_required == "T") else False

        return tfa_required

    except Exception as e:

        # return None to indicate that validation could not be performed

        return None

        

def validate_code(user, code):

    try:

        # get thr user record

        for row in db(db.auth_user.email == user.get('email')).select():

            return code == pyotp.TOTP(row.secret).now()

    except Exception as e:   

        # return None to indicate that validation could not be performed

        return None

        

def qr_code(user):

    try:

        for row in db(db.auth_user.email == user.get('email')).select():

            url = pyotp.totp.TOTP(row.secret).provisioning_uri(name=row.email, issuer_name='RopeInspector App')

            qrcode = segno.make(url)

        return qrcode.svg_data_uri()

    except Exception as e:   

        # return None to indicate that validation could not be performed

        return None


auth = Auth(session, db, define_tables=False

    extra_fields=[

        Field('tfa_required'

            label=T("TFA-Code"),

            type='string'

            length=1

            default="F", filter_in=lambda x: "T" if (x) else "F"

            requires=IS_EMPTY_OR(IS_LENGTH(6,6))

        ),

        Field('secret', type="string", default=f"{pyotp.random_base32()}", writable=False, readable=False),

        Field('qr_code'

            label=T('Scan this QR-Code'),

            type='text'

            compute=lambda user: qr_code(user), 

            writable=False

            represent=lambda url: IMG(_src=url,_width="256",_height="256"))

    ],

    two_factor_send=lambda user, code: "55555", # Necessary to activate 2-factor auth

    two_factor_required=tfa_is_activated, 

    two_factor_validate=validate_code)

auth.use_username = True

auth.param.registration_requires_confirmation = settings.VERIFY_EMAIL

auth.param.registration_requires_approval = settings.REQUIRES_APPROVAL

auth.param.login_after_registration = settings.LOGIN_AFTER_REGISTRATION

auth.param.allowed_actions = settings.ALLOWED_ACTIONS

auth.param.login_expiration_time = 3600

auth.param.password_complexity = {"entropy": settings.PASSWORD_ENTROPY}

auth.param.block_previous_password_num = 3

auth.param.default_login_enabled = settings.DEFAULT_LOGIN_ENABLED

auth.define_tables()

auth.fix_actions()

auth.logger = logger


flash = auth.flash


P.S. Contrary to the documentation, the function two_factor_send must be defined to activate TFA.

Best regards
Stefan

Dave S

unread,
Jan 4, 2026, 8:50:28 PMJan 4
to py4web
On Sunday, January 4, 2026 at 4:25:45 AM UTC-8 Stefan Messmer wrote:
Here is the code that works for me (at the moment, modified common.py from Scaffold app). It is surly  not perfect, but it has the key features. I added the extra_fields tfa_required, secret and qr_code to the auth_user db. The secret is generated on the first login and is no writable. The field qr_code is computed and the field tfa_required is used to keep track of TFA required status. If the user leaves it empty, it is set to "F", meaning no TFA is required. If the user enters a 6 character code, it is set to "T" (TFA required). Unfortunately I cannot verify the correct code at the moment (needs a validation function). If anybody has an idea how to implement this feature, I would appreciate very much. The codes are generated with pyotp and the QR-code is generated with segno. I have tested the TFA with Authy and Google Authenticator. I

# #######################################################

# Instantiate the object and actions that handle auth

# #######################################################

def tfa_is_activated(user, request):

    try:

        # get thr user record 

        tfa_required=False

        for row in db(db.auth_user.email == user.get('email')).select():

            tfa_required = True if (row.tfa_required == "T") else False


auth only allows 1 email for a user?  That seems plausible, but then you should only get rows[0] from the select, and the for seems excessive .  If there's a use case for allowing more than one email, the for is going to give you the result from the "last" one (for whatever order the DB engine chooses).
[earlier thread posts elided]

/dps

Dave S

unread,
Jan 4, 2026, 9:01:19 PMJan 4
to py4web
The other comment is I feel I can understand the code pretty well even without being familiar with the new generation auth or with pyopt
 (I do have some book learning on OpenAuth, but no hands-on with it)

/dps
 

Stefan Messmer

unread,
Jan 6, 2026, 2:12:59 PMJan 6
to py4web
You are right, of course. However, I have chosen the version with for ... in ... to avoid an error if no match is found in the database and to return "False" and not "None" in this case.

Best regrards
Stefan 

Dave S

unread,
Jan 6, 2026, 6:36:28 PMJan 6
to py4web
On Tuesday, January 6, 2026 at 11:12:59 AM UTC-8 Stefan Messmer wrote:
You are right, of course. However, I have chosen the version with for ... in ... to avoid an error if no match is found in the database and to return "False" and not "None" in this case.


Yes, your code is clear about being able to do that.

/dps
Reply all
Reply to author
Forward
0 new messages