Ensuring a single login per user

Hello Friends:

I was going to do something like the following (which is crude just to show the example here) to ensure a single-login per user.

# ====================================================
# If not None, user is already logged in. Again, crude.
# ====================================================
if anvil.users.get_user():
      alert(..., dismissable=False)      # Alert user that he's already logged in elsewhere.
      open_form('Forms.LandingPageForm') # Send back to landing page.
      anvil.users.logout()               # This step may not be a good idea b/c may log user out everywhere.

BTW: I’ve decided to use Magic eMail Link method.

But then I began wondering if the items available via anvil.users (shown in the following screenshot) might be helpful for this, and perhaps be more rigorous / bulletproof (for instance, methods of the UserExists object). Unfortunately, I don’t know where to find the API for the attributes shown:

  1. Can someone point me to the API docs for these?
  2. How do you personally ensure a single-login session per user? Is it conceptually as simple as I’ve shown?

As always, thank you!

EDIT: Actually the above snippet is faulty b/c we can’t tell if the return from anvil.users.get_user() represents the first login or the Nth. But I think the community understands my need. :blush:

I would use user context information, store it in the users table during login, and periodically check (when using many/any server call) to make sure that context matches. If it does not, automatically log out the user.

When a user logs in successfully it should overwrite any previous context info, kicking off the ‘old’ user. I doubt this would stop multiple people from the same office using the same browser from using the same login though.

Many other solutions can leave users ‘logged in’ to sessions they already closed out of, making it impossible for them to get back in.

Similar to code found in these threads:

I guess this scenario is fine.

This looks quite good. A couple of follow-ups on this:

  1. Are you storing the anvil.server.context.client.location object itself or either a stringified or key/value representation of it? If object form, the check could be if object1 is not object2: logout(...). Or, if a stringified or key/value representation of it is stored instead, compare the attribute values one-by-one and execute logout(...) if they don’t line up.

  2. As for when to periodically check, your suggestion is that natural check-points are just prior to anvil.server.call(...) invocations, and execute logout(…) if the two objects, or strings, or k/v pairs no longer match. Yes?

  3. Finally, there’s no need to “managethe context information stored in the users table, because we’re simply allowing the latest successful login overwrite it. All we do is step 2 (periodically compare). Yes?

Thank you.

I just checked myself and yes, I did have to turn it into a string, since whatever object anvil uses is not serializable into a simple object for use in a simple object column. So you could use a text column instead.

  user_row = app_tables.users.get_by_id(anvil.users.get_user().get_id())
  user_row['context'] = str(anvil.server.context.client)

I have not built it out, but if I were to, I would have a function take the context object “string” and md5 hash it so I didn’t unnecessarily store user info I did not need and put it in a text column in the users table. This function would run in a server call alongside a successful anvil.users.login_with_form().

I would then create a server module security function that would compare the current context object hashed string, with whatever was in the table, and if it did not match, run anvil.users.logout().

I might even create an @decorator for this purpose.

from functools import wraps

def with_user_security(func):
  
  @wraps(func)
  def wrapper_func(*args, **kwargs):
    if not your_context_compare_function_here():
        anvil.users.logout()
        #return #  ?? how you want to handle security is up to you
    func(*args, **kwargs)
  return wrapper_func

then later:

@anvil.server.callable
@with_user_security
def any_callable_nmvega_function():
    ...

So yes,

It should handle itself, if you have most of the important bits of whatever you are doing protected inside the server modules. (Otherwise you really don’t need/have any security anyway)

3 Likes

@ianbuywise I understand your solution and like it. @jshaffstall in a PM liked it, too. (I ratted :rat: him out. LoL). But can’t tell you how much I appreciate you guys and the community.

Thank you very much!