Please note that this kind of "remember me" functionality can represent a potentially security hole. It makes sense for some sites where the convenience out weighs any problems that would occur if someone fraudulently gains access to the site. As I wrote this for a site that is concerned with playing chess online it seemed worth it.
So to get started this is meant to work with:
Also note that I've left some of the imports as they appear for my app (chrss), so you'll need to change them as appropriate.
Conceptually a regular request with a remember me feature works thus:
- If the user is not logged in, we check for a "remember me" cookie
- If the cookie is present then we check to see if it matches a token (which maps to a user) in the database
- If there's a match to a user we can login the user and on future requests we can ignore the remember me cookie (everything works as before)
The token in the database is randomly generated when the user logs in (with the "remember me" option ticked on the login form) in a similar way to any kind of session tracking cookie. The different is that the token/cookie is meant to hang around for much longer than a regular session. It's used in addition to Turbogears tg-visit cookie and is just a handy shortcut for logging in a user automatically. This means that it's fairly non-invasive in so far as it interacts with the Turbogears identity framework.
First of all we need a table in the database to connect the remember me token to a user. So in my models I defined the following entity:
class RememberMe(SQLObject): user_token = StringCol(length=40, alternateID=True, alternateMethodName="by_user_token") user_id = IntCol() expiry = DateTimeCol() expiry_index=DatabaseIndex(expiry)
The rest of the code then lives in remember_me.py.
First there's the code to "remember" a user. This creates a RememberMe entity and sets a cookie on the user's machine:
def generate_token(): key_string= '%s%s%s%s' % (random.random(), datetime.now(), cherrypy.request.remote_host, cherrypy.request.remote_port) return sha.new(key_string).hexdigest() def remember_user(user): from chrss.model import RememberMe user_token=generate_token() expiry=datetime.now() + timedelta(days=remember_me_age_days) remember=RememberMe(user_token=user_token, user_id=user.id,expiry=expiry) cookies= cherrypy.response.simple_cookie max_age = remember_me_age_days*24*60*60 cookies[remember_cookie_name] = remember.user_token cookies[remember_cookie_name]['path'] = '/' cookies[remember_cookie_name]['expires'] = formatdate(time() + max_age) cookies[remember_cookie_name]['max-age'] = max_age
Here's the reverse function to "un-remember" a user (which you would call from your logout method):
def unremember_user(user): cookies = cherrypy.request.simple_cookie if remember_cookie_name in cookies: user_token=cookies[remember_cookie_name].value if user_token: from chrss.model import RememberMe try: remember=RememberMe.by_user_token(user_token) remember.destroySelf() except SQLObjectNotFound: pass # now clear cookie cookies= cherrypy.response.simple_cookie cookies[remember_cookie_name] = '' cookies[remember_cookie_name]['path'] = '/' cookies[remember_cookie_name]['expires'] = 0 cookies[remember_cookie_name]['max-age'] = 0
Before I get onto the two monkey patches, we need to make one more function, that we use to login the user given a user entity (bypassing the need for their username and password) and is based on code from here:
def login_user(user): ''' from http://docs.turbogears.org/1.0/IdentityRecipes''' visit_key = turbogears.visit.current().key IdentityObject = turbogears.identity.soprovider.SqlObjectIdentity from chrss.model import VisitIdentity try: link = VisitIdentity.by_visit_key(visit_key) except SQLObjectNotFound: link = None if not link: link = VisitIdentity(visit_key=visit_key, user_id=user.id) else: link.user_id = user.id user_identity = IdentityObject(visit_key); return user_identity
The monkey patches
Now we get to the meat of the code - the bit which does the actual "magic". In both cases we are monkey-patching methods that belong to the IdentityVisitPlugin class in Turbogears (defined in turbogears.identity.visitor).
First up is identity_from_visit which normally just looks for the tg-visit cookie and then sees if that's associated with a user login or not. We shall effectively override it, so that if no association is found then we will perform a further check to see if there is a remember me cookie that will let us log the user in:
# keep a reference to the original function old_identity_from_visit=turbogears.identity.visitor.IdentityVisitPlugin.identity_from_visit def identity_from_remember_me( self, visit_key ): identity=old_identity_from_visit( self, visit_key ) if identity.anonymous: # not logged in so check for remember me cookie cookies = cherrypy.request.simple_cookie if remember_cookie_name in cookies: log.info("checking remember me cookie") user_token=cookies[remember_cookie_name].value from chrss.model import RememberMe, User try: remember=RememberMe.by_user_token(user_token) user=User.get(remember.user_id) return login_user(user) except SQLObjectNotFound: pass return identity # monkey-patch the method turbogears.identity.visitor.IdentityVisitPlugin.identity_from_visit=identity_from_remember_me
The next method we patch is identity_from_form. For this we just check whether there is a "remember_me" parameter in the request after a successful login (from calling the original method) and if so call the remember_user() function.
old_identity_from_form=turbogears.identity.visitor.IdentityVisitPlugin.identity_from_form def identity_from_form(self, visit_key): identity=old_identity_from_form(self, visit_key) if identity is not None and not identity.anonymous: # login worked, so now see if 'remember me' set params=cherrypy.request.params remember_me=params.pop('remember_me', None) if remember_me: remember_user(identity.user) return identity turbogears.identity.visitor.IdentityVisitPlugin.identity_from_form=identity_from_form
You'll just import the remember_me module early on in starting up your Turbogears app and it will apply these monkey patches. Then if you modify your login template to include a "remember_me" checkbox you should have everything working.
As I said before it's fairly non-invasive (as far as monkey patches go), so there shouldn't really be a need to modify much beyond your login form and to add a call to unremember_user to your logout code. The only other thing is perhaps to setup a cron-script or other background task to delete expired entries in the database (which is why the RememberMe entity has an expiry column).
The remember_me module is available for download here.