Auth - adding Google plugin to my app creates duplicate user accounts

77 views
Skip to first unread message

pe...@anewalt.com

unread,
Mar 18, 2026, 10:00:57 AMMar 18
to py4web
My app has used default/password-based auth for a while.  I would like to add other auth plugins, Google for example.  I can get it working fine, but have an issue.  

I have auth.use_username=False, so email is used as the username.  If an existing user /email with a password now chooses to authenticate using Google instead, a new user account is created for that user.  Now there are 2 auth_user rows with the same email.

Ideally, the same user (identified by email address) could be authenticated in more than one way.    In practice, I would not expect them to switch between the 2 - rather, they see that Google authentication is now available and decide to use that.  So I thought that perhaps there was a reason why the 2 cannot co-exist, but I tested this by copying the sso_id from the new account into the record for the old account, and deleting the new account.  I then tried to login with both methods, and both work fine.

So it should all work as I want, but the duplicate-account creation in auth_user is a bother.  How can I stop that from happening?  

Ali

unread,
Mar 22, 2026, 3:05:27 PMMar 22
to py4web
I had a similar issue, but realized the email address used by the user in question was not exactly the same when trying the two methods. Gmail is indifferent to "dots".

I know this may not be the answer you are looking for. Sharing just in case. 

pe...@anewalt.com

unread,
Mar 23, 2026, 9:13:59 AMMar 23
to py4web
Thanks - based on this i tested again and checked very carefully whether there was any difference in the email address between the original and the newly-created rows.  There is not, they are identical.

pe...@anewalt.com

unread,
Mar 23, 2026, 10:36:25 AMMar 23
to py4web
OK, doing some tracing of activity in auth.py, specifically get_or_register_user, I find that a user record is being queried using the sso_id returned by Google, which fails (because the existing user has not logged in using SSO before & therefore doesn't have an sso_id in their record).  There is an "elif email" statement, which never gets executed since the sso_id was present in the token supplied by Google, so it then proceeds to create a record for the "new" user.

Massimo, can this be changed to check the email in the Google token if the sso_id check fails?


pe...@anewalt.com

unread,
Aug 19, 2026, 6:03:08 PMAug 19
to py4web
This is still broken in 20260805.  For anyone who may encounter this, here is a fix.

Code in auth.py that causes duplicate auth_user rows with identical emails for any user with a password who then logs in with Google:
    def get_or_register_user(self, user):
        db = self.db
        # if we have an sso_id we use it to id the user
        if user.get("sso_id"):
            keyid = "sso_id"
            row = (
                db(db.auth_user.sso_id == user["sso_id"]).select(limitby=(0, 1)).first()
            )
            # the sso source is always more authoritative so update the record
            if row:
                row.update_record(**user)
                # pass the full user
                user = row.as_dict()
        # otherwise we id the user via email
        elif user.get("email"):
            keyid = "email"
            # return a user if exists and has a verified email
            row = self.get_or_delete_existing_unverified_account(user["email"])
            # the database is more authoritative
            if row:
                user.update(**row.as_dict())
        else:
            return None

I have replaced the above with the following code that checks for the successful retrieval of an auth_user record using sso_id but then reverts to email if that fails (because the first time that user logs in with Google they won't have an sso_id saved):
    def get_or_register_user(self, user):
        if not user.get("sso_id") and not user.get("email"):
            return None
        db = self.db
        row = None
        # if we have an sso_id we use it to id the user
        if user.get("sso_id"):
            keyid = "sso_id"
            row = (
                db(db.auth_user.sso_id == user["sso_id"]).select(limitby=(0, 1)).first()
            )
            # the sso source is always more authoritative so update the record
            if row:
                row.update_record(**user)
                # pass the full user
                user = row.as_dict()
        # otherwise we id the user via email
        if not row:
            keyid = "email"
            # return a user if exists and has a verified email
            row = self.get_or_delete_existing_unverified_account(user["email"])
            # the database is more authoritative
            if row:
                user.update(**row.as_dict())

Reply all
Reply to author
Forward
0 new messages