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 ā€œmanageā€ the 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

@ianb 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!