Login Tools

User Login and Authentication

Author: Michael Foord
Contact: fuzzyman@voidspace.org.uk
Author Homepage:
 Pythonutils
Version: 0.6.2
Date: 2005/11/02
License:BSD License
Home Page:Login Tools Homepage
Online Demo:Jalopy and login demo
Support and Bug Reports:
 Pythonutils Mailing List

Contents

1   Introduction

logintools is a set of scripts for handling user accounts and authentication. It is designed to work with Python CGIs, and can be added to any web application with as little as two lines of code. It acts as a whole login framework, and provides new user sign ups, administrator login, and user 'edit account' features. It includes a mechanism for storing/accessing additional user preferences.

logintools uses HTML templates for the various screens, meaning it can fit into any look and feel of website/application. By storing templates and the config file in different directories, a single install can handle logins for many applications. Applications can share a user base or have separate ones. They can also share html templates or have a separate directory. Various aspects are configurable - like whether or not new users can sign up.

There are generally two different reasons you might want to make users login. You might have information, or an application, that you want to restrict access to. Alternatively, you may want to tailor an application to the specific user - this includes recording user preferences and tracking who has done what. You can achieve both of these with a user login. Doing this needs a whole framework - storing preferences, administering accounts, allowing users to change their password, and so on. Basic authentication on the server is one solution - but it's difficult to integrate with CGI. logintools provides a whole framework to do this.

At the moment logintools comes distributed in a single zip file along with the Jalopy project. Jalopy is a collaborative web project that uses logintools. It gives you a website with a WYSIWYG online editor so that several people can edit the website. The private index to do this is protected by logintools - there is also a public index to just view the pages. Jalopy provides a very nice example of how to use logintools. Adding the login framework to Jalopy was really as easy as adding the following 2 lines of code :

from logintools import login

# this function presents the user with a login screen (and exits)
# if they are not already logged in
action, userconfig = login(theform, userdir)

There are a couple of other places where Jalopy prints links to log out, edit accounts and enter the admin screen - but basically that's it. The userconfig returned is an object that represents this users preferences. The action value is a special value that is described in more detail later.

logintools works as described - but is still alpha quality. I will keep API changes to a minimum, but I don't guarantee this. Particularly, the way it uses cookies is likely to change. Bug reports welcomed, as are suggestions. The standard logintools package (and Jalopy) could be made to look a heck of a lot nicer if I had some decent HTML templates. If someone wanted a way to contribute, this would be a very good way !

logintools is designed to be used with Python 2.2 or later.

2   Downloading

The Jalopy and logintools package can be downloaded from Jalopy and logintools Zip (636k).

You can try out an online demonstration of Jalopy and logintools at Jalopy and login demo.

The demo should have an administrator login. This means you can create and edit user files. Obviously any logins you create can also be edited and deleted by other people. It would be helpful if you didn't change the password of the admin login.


3   Installing

Setting up login tools to work with your scripts is a several stage process. They're all simple though :

3.1   Installing the Modules

logintools is dependent on several modules. (which are all generally useful by the way). By default logintools comes in a directory called 'modules' which contains all it's dependencies. To use them, add the modules directory to your sys.path :

import sys
moduleDirLoc = '../modules'
sys.path.append(moduleDirLoc)

See the directory structure of the Jalopy distribution if you want an example of how this works. The advantage of this is that other programs (especially Jalopy !) can also access these modules.

Once you have installed the modules like this they can be used by several programs. Each program can have a separate 'user directory' but share the modules.

3.2   User Directory

Every person who has (or creates) a login will have a single config file that stores all their details. These are stored in the a directory referred to as the 'user directory' and are named 'login name' + '.ini'. If you have several scripts using logintools on the same server, they can either share a user directory or have different ones. The user directory also needs to contain the basic config files. See the 'users' directory that comes in the standard distribution. It's in the jalopy directory, because it's the user directory for the Jalopy program. This contains the following files :

  1. config.ini - the configuration file for this setup.
  2. default.ini - the template used to create new users. It contains some default values.
  3. admin.ini - an example admin login. This is setup as the main administrator in config.ini.
  4. guest.ini - an example user login.
  5. temp.ini - used to store pending (unconfirmed) users.

Every time you call the login functions, you'll need to tell it where to find this user directory. From this it can access all the users, and the config file. The config file tells it where to find the HTML templates as well as configuring the behaviour of logintools. Having a file called 'admin.ini' means their is a login with the username 'admin'. Having a file called 'guest.ini' means their is a login with the username 'guest'. (The other files are reserved names, not logins). These two default users start with the password 'pass1'. It is recommended that you change the name of the admin login by changing the filename (and the 'username' value inside the file). You can change the password from the 'edit account' screen.

3.3   The Config File

Have a look in 'config.ini' in the users directory. You'll see it's comprised of various keywords and corresponding values. The config file, and all user files, are all basic config files. As you will discover in a bit, they are in a format specially for use with a Python module called ConfigObj :

templatedir = 'logintemplates/'             
cookiepath='/cgi-bin/jalopy'                
adminuser='admin'                           
newloginlink='Yes'                          
adminmail='fuzzyman@voidspace.org.uk'       
email_subject = 'New Jalopy Login'          
email_message = '''You have signed up for, or been invited to join Jalopy.
Clicking on this link will confirm your new login.'''      

3.3.1   templatedir

The most important value here is the 'templatedir' one. This tells logintools where to find the HTML templates. When the login functions are called, the current working directory will usually be the directory that the main calling script is in. The templatedir location should either be an absolute location, or relative to the current working directory of the calling script. If several scripts in different locations share the user directory, then it will need to be an absolute location.

3.3.2   cookiepath

logintools uses a cookie to store whether or not a user is logged in. An obfuscated hash of the password is stored in the cookie, so it's difficult to forge. It does mean that at the moment logintools doesn't play well with applications that use cookies. It's easy to program round this or for me to change logintools, if it's a problem for anyone.

The cookie path tells the browser when to send our cookie to the server. If logintools is the only application in your domain setting a cookie then you don't need to worry about cookiepath. You could set it to '/' - which means send it with every request. If you had other applications that (potentially) set cookies, you need to set it to a unique path that identifies your application. In the default example it will only return the cookie when you access URLs that start '/cgi-bin/jalopy'. This also allows you to have several applications using logintools on the same server. With different cookiepaths they can all co-exist without interfering.

I am likely to get rid of the requirement to have a unique cookiepath per application and replace it with a name for each setup. This name will be stored in the config file instead of the cookie path. (This will mean you can have several applications with separate cookies but the same cookiepath.

3.3.3   adminuser

This value is the login name of the main admin user. This user can't be edited or deleted by any other administrator.

3.3.4   newloginlink

This option determines whether or not the 'create new login' link appears on the main login screen. This value should be set to 'Yes' or 'No'. If it is 'No' then users are unable to sign up for new accounts and the link doesn't appear. You are still able to create new accounts or invite new users through the admin interface.

3.3.5   adminmail

If 'newloginlink' is 'Yes' then users can sign up for new accounts. After entering their details, they will be emailed a link to confirm the new account. This makes creating new logins automatically harder. If adminmail is set - that email address will be mailed whenever a new account is confirmed. Set it to '' to switch this off.

3.3.6   email_subject & email_message

These two values make up the message that a new user is sent. It is sent to a user when they create a new account or they are invited by an administrator.

3.4   The HTML Templates

Having set up a user directory with a 'config.ini' file you will have specified a directory that contains all the HTML templates. The one in the standard distribution is the directory called 'logintemplates' in the 'jalopy' directory. This contains five HTML files and seven text files. These are all used to generate the different screens used by logintools. This includes the main login screen, creating a new account, editing your account details and all the admin screens.

Each template file contains the HTML framework and some values that are filled in by the logintools scripts. When you edit these files you must keep these values in. These values all look like this - **some value**. For a full overview of what each file is for, and what all the values are for, check out the template files and values section. Many of the pages consist of a basic page (filename usually ending in .html in the template directory) with a form to be inserted into it. This form will usually be a file with a name ending .txt in the template directory.


4   Login Tools in Your Scripts

Integrating login_tool into your scripts is very easy. Exactly how depends on how you want your application to work. There are three different possibilities :

  1. You want to prevent people from using a script unless they are logged in. No Login, No Access
  2. You want to prevent people from using part of a script unless they are logged in. Protecting Part of An Application
  3. You want to provide a different view or different options if they are logged in. Is the User Logged In ?

The basic function that handles everything is the 'login' function. The first case is the simplest to deal with, so I'll illustrate login with that. It uses the login function - in many cases it might be the only thing you need to import from logintools - from logintools import login.

4.1   No Login, No Access

logintools uses cookies for user authentication. If the user doesn't have a valid cookie then calls to the login function display the login screen and then call sys.exit(). The login screen is made up with the login_page.html template file. This has the newlogin_nojs.txt form inserted into it.

The user then has a choice. If they already have a login they can sign in with their username and password. Otherwise, assuming new logins are enabled for your application, they can hit the 'create new login' link. Either of these choices will result in a form submission to your script with a login parameter set. Your script must detect that this access is destined for the l ogin function (because it has a login parameter) and pass it to the login function. However, because we are in situation1 - we want to trap all accesses anyway. So the first thing your script ought to do is to call the login function. If the access results from the user doing something within the logintools, then it will be picked up automatically. Assuming you have chosen a user directory this can be done with the following code :

import cgi
import sys
import os
sys.path.append('../modules')
from logintools import login

thisscript = os.environ.get('SCRIPT_NAME', '')
action = None
userdir = 'users'
theform = cgi.FieldStorage()
action, userconfig = login(theform, userdir, thisscript, action)

If your script always starts with something like this, then the user must login or the script exits. Nice and easy. The login function returns two values - action and userconfig. If you are doing things this way, you probably don't need to use the action value. In this case you can slightly simplify your call to the login function :

action, userconfig = login(theform, userdir)

When you call login, action defaults to None and thisscript defaults to the environment variable 'SCRIPT_NAME'. This is the actual CGI script that the user called - and is put into any URLs generated by logintools. You can use the 'thisscript' parameter to divert new calls to a different CGI script. userconfig is an important creature. userconfig is an object that represents the users account. It is actually an instance of ConfigObj - which behaves like a dictionary. See the user config files sections for more details of the values available in it. You can use it to get the email address, real name and the login name of the current user. :

username = userconfig['username']
realname = userconfig['realname']
email = userconfig['email']

So long as you avoid all the keys that logintools uses, you can also use this object to store other settings etc for the user. You could use it to implement session persistence, or just save user preferences. There are lots of special methods and attributes associated with ConfigObj - but the important one for us is the write method. Simply treat the object as a dictionary and call write to save your changes. :

userconfig['favourite colour'] = 'red'
userconfig['favourite number'] = '3'
userconfig['list of items'] = ['choice 1', 'choice 2', 'choice 3']
userconfig.write()

ConfigObj instances can store strings or lists of strings. You can also nest sections, which you access/set as sdictionaries.

Something else that you ought to note. Because logintools uses the 'login' parameter to tell if the current call is meant for it you mustn't use 'login' as one of your cgi parameters. The same is true of 'action'. This is used to store any action value you pass to login. 'login' and 'action ' are reserved parameters. See The Action Value section for the lowdown on 'action'.

4.2   Protecting Part of An Application

An alternative use of authentication is to just protect part of an application with a login. For example you may want people to be able to browse a dynamically generated list of files - but only be able to download them if they are logged in. In fact it's possible that the call to the login function could come from several different places in your script. The problem is, that when the login screen is displayed and the user enters their password - the script is restarted. Such is the nature of CGIs. What you need is some way of knowing what the user was doing (or trying to do) when they logged in. Also, if the user selects something like an 'edit account' option then the application will be restarted with an instruction to the login function to go to display the appropriate 'edit account' screen. Your application must intercept this call and pass it straight to the login function.

So we have two needs. The first is to know where in your application a call to logintools came from. The second is to always trap calls that are intended for the login function. Luckily the answers to both these problems are intertwined into a single solution. The answer involves using the action value to tell you where the call came from. Any calls to your script that use the reserved parameter 'login' need to be passed immediately to the login function. This returns your original 'action' value back to you - and tells you where the user came from. This is all explained in detail (with examples), in the section entitled The Action Value. In summary, any value you pass into the login function will be preserved throughout the whole login process and returned once a successful login has been achieved. So if you pass into action something that identifies where login is called from, when you trap the call to your script right at the start - this value will be returned to you. This tells you which part of your program you need to go to. Confused ? Go and read The Action Value section.

# start of the program
if theform.has_key('login'):
     # only called if we are in the middle of a login process
     action, userconfig = login(theform, userdir, thisscript)
     if action == 'part1':
         call_part1(userconfig, theform)
     elif action == 'part2':
         call_part2(userconfig, theform)
     elif action == 'part3':
         call_part3(userconfig, theform)

4.3   Is the User Logged In ?

A third alternative is to provide users with a different view, or options, depending on whether they are logged in or not. This means you need a function that will tell you. isloggedin does the job. It returns False if the current user isn't logged in, or a (userconfig, newcookieheader) tuple if they are. You can print the newcookieheader if you want - but it's not absolutely necessary. As a simple example :

from logintools import isloggedin
test = isloggedin(userdir)
if not test:
    print "The user isn't logged in."
else:
    userconfig = test[0]

We really ought to test for the login parameter as well, in case they have just logged in, or are even creating a new login. A more practical example might look something like :

import cgi
from logintools import login, isloggedin
theform = cgi.FieldStorage()
userconfig = None

if theform.has_key('login'):
    action, userconfig = login(theform, userdir)
else:
    test = isloggedin(userdir)
    if test:
        userconfig = test[0]
if userconfig:
    username = userconfig['username']
else:
    username = 'Guest User'

If the current user isn't logged in then userconfig will be set to None, if they are logged in then userconfig will be the user config file. If we had only called the login function, then the user wouldn't be able to see anything other than the login screen. A useful thing to do is to display a link to the login screen if the user isn't logged in. If they are logged in then there are various other options you may want to provide links to. The next section Links to the Other Options shows how to do this.

You could also include the login form within your own page - the login form is in the login_nojs.txt file in the logintemplates directory. It is expected that you will edit this form to fit in with the appearance of your own pages, but don't forget it is used by the displaylogin function as well.

4.4   Links to the Other Options

The function login that we have mainly used so far, only gives access to the login screen and create a new login screen. logintools also includes a framework for the user to edit their account, an admin login to administer all the accounts, and more besides. These can be accessed by requests encoded into a URL - in other words, by ordinary links. As we have already seen, any program that uses logintools must trap calls that use the login parameter and pass them to the login function. The value that we pass in the login parameter determines what happens. Assuming myscript.py is the path to your script, the following links below would cause the noted behaviour :

myscript.py?login=showlogin - display the login screen

myscript.py?login=logout - logout the current user

myscript.py?login=newlogin - enter the create new login screen

myscript.py?login=confirm&id=ID_VALUE (sent in an email) - confirm a pending login

myscript.py?login=editaccount(&action=ACTION_VALUE) - edit an account, optional action value.

myscript.py?login=admin(&action=ACTION_VALUE) - enter the admin screens, optional action value.

Some of these links will only work in certain situations. For example, the admin link will only work if the currently logged in user has admin rights. The confirm a pending login link will only work if the ID_VALUE is a valid one. The create a new login one will only work if new logins are enabled on this setup.

You can see that the last two links (edit account and admin screens) can have an action value added to them. If you add these links dynamically from your script, you can set the action value in the normal way to tell where the call was made from. This means when the user has finished editing their account, you can return them to where they were before.

A side effect of using GET requests (requests in URLs), is that the user can bookmark the 'edit account' screen. If they return to it without being logged in - logintools will ask them to log in first, and then put them into the edit account screen. It uses the action value internally to do this.

This is all used to good effect in Jalopy. After you log into Jalopy it presents you with the main Jalopy screen. It has a logout link and an edit account link. If you have admin rights it will also display the admin screen link. It does this by testing the admin level in the userconfig. It also prints a personalised greeting to who ever is logged in by reading 'realname' from the userconfig. An easy way to generate a link to your script is to use the os.environ['SCRIPT_NAME']. :

url = '<a href="%s">%s</a>'
thisscript = os.environ['SCRIPT_NAME']
logoutval = thisscript + '?login=logout'
edaccval = thisscript + '?login=editaccount'
adminval = thisscript + '?login=admin'
if action:
    # either use a url safe value or use urllib.quote_plus(action)
    actionline = '&action=' + action
    logoutval += actionline
    edaccval += actionline
    adminval += actionline

logout_link = url %(logoutval, 'Log Out')
edacc_link = url % (edaccval, 'Edit Account')
if int(userconfig['admin']) > 0:
    # or just if int(userconfig['admin']):
    admin_link = url % (adminval, 'Admin Screen')
else:
    admin_link = ''

Below is a similar chunk of code from jalopy. It isn't so straightforward because it's doing replace in the template file, but you can see the end part of the URL being added. If the userconfig['admin'] value is greater than '0' then the comments around the admin link are removed and the link filled in. You can see that in jalopy I don't use the action value. In which case it is perfectly safe to miss it out altogether. :

privatepage = privatepage.replace('**logout**', thisscript+'?login=logout')
privatepage = privatepage.replace('**edit account**', thisscript+'?login=editaccount')
privatepage = privatepage.replace('<!-- **commstart** ', '')
privatepage = privatepage.replace(' **commend** -->', '')
if int(userconfig['admin']):
    privatepage = privatepage.replace('<!-- **admincommstart** ', '')
    privatepage = privatepage.replace(' **admincommend** -->', '')
    privatepage = privatepage.replace('**admin menu**', thisscript+'?login=admin')

If you have any questions, about this or anything else, then feel free to ask. The best place to ask questions is the PythonUtils Mailing List.


5   User Config Files

We have already mentioned that the user config files are ConfigObj instances. We have also said that you mustn't overwrite any of the keywords that logintools uses. To see what these are I could tell you to look at guest.ini or admin.ini in the 'users' directory. However, I'll also list them here with a brief explanation for each.

You are free to add and remove any other keywords to the user config. You can save them by calling userconfig.write().

5.1   Creating New Accounts

You will notice the file 'default.ini' in the 'users' directory. This is a blank user config file. New accounts are either created by new users signing up or from the admin interface. In either case new accounts are created by copying 'default.ini' and then filling in the values from the users choices. New accounts that require email confirmation (link sent to email address provided) are stored in 'temp.ini' until they are confirmed.

Minimum details needed to create a new account are :

  • password
  • login name
  • real name
  • email address

(Note that the following user names are reserved - 'config', 'default', 'temp').

If you need to be able to create accounts from your program you can import and use the 'createuser' function. This doesn't check first if the username already exists ! (So you could use it just to change the values) :

from logintools import createuser
createuser(userdir, realname, username, email, password)

5.1.1   Values in Default User

The default user ('default.ini') contains all the values in a normal account. Most of these will be overwritten by user choices, some are filled in as the account is created/used. The ones you need to specifically set to your preferred values are :

  • max-age
  • admin
  • editable

5.2   Extending the User Config File

It is also possible to add your own values to 'default.ini' - these will automatically be given to any new accounts. Add to this the fact that you are always returned the userconfig file from the login function (and the isloggedin function) as well as the ultra-simple method of changing/saving it - makes the userconfig file a logical place to store user preferences.

The logintools framework has already got built into it an experimental method for fetching additional values from the user when they log in. In order for it to be fully useful it needs to be extended though. These values need to be accessible in :

  1. User sign up
  2. edit default user
  3. admin create/invite new user
  4. user edit account screen

At the moment I've only implemented number 1. In addition to this there would need to be a way of specifying which values the user should be allowed to edit. (As ConfigObj can store lists, that wouldn't be so hard). As it would be a fair amount of work I will only extend this if someone requests it and is willing to work on testing it with me. See Keyword Lists in templates for what I've done so far.

It would get round a problem though. At the moment the administrator can change the login name of accounts or delete accounts. In addition to which users may need to be able to sign up for new accounts. If you aren't using the user config to store user preferences - but are storing them somewhere else, e.g. a database - then there is a problem keeping the logintools details in sync with the rest of your details. The easiest way round this would be to allow the programmer (you) to register functions to be called when any of these things happen. Again - if anyone needs this, and is willing to help me test it, then get in contact with me.


6   Using Login Tools

The appearance, and to an extent the functionality, of all aspects of logintools is dependent on the HTML templates. This section just lists the different screens that logintools makes available.

6.1   As a User

Using logintools should be very easy - all the processes are familiar. Below are the various parts of logintools accessible to the user.

6.1.1   Login Screen

This is the default screen. It is presented to the user if the login function is called without parameters and the user isn't logged in. It can also be presented by calling the displaylogin function directly or by calling login with 'login=displaylogin' set. If new logins are enabled, it will normally show a link to create a new user account.

6.1.2   Sign Up For a New Account

It is only possible for users to sign up for new accounts if it is enabled in 'config.ini'. Clicking on a link similar to myscript.py?login=newlogin, will direct the user to a form asking for various details to create a new account. Details asked for so far are :

  • password and confirmation password
  • login name
  • real name
  • email address

Once the user has filled this form in, a confirmation email is sent to the email address they provide. Clicking on the link sent in this email will confirm the account and drop them straight into the application.

6.1.3   Edit Account

This is only available to the user if their account is 'editable'. Options they can change are :

  • password
  • real name
  • email address

Any email address they enter will be checked for basic validity, but will not need to be confirmed.

6.1.4   Log Out

Not really an alternative screen. If this is done, the users login cookie is cancelled and they are dropped back to the login screen.

6.2   As an Administrator

If a user has admin rights, clicking on a link like myscript.py?login=admin will drop them into the admin menu. This gives them access to various screens to administrate the setup and user accounts. Providing a link like this is the job of the programmer. It is simple to test if a user has admin rights, and if they do provide a suitable link, from within the application.

6.2.1   Edit Config File & Default User

This menu option provides access to the options that affect the whole setup. Along with the form to change the values is an explanation of what the values are for. The Config File Values affect the way that logintools behaves, the Default User Values are values that are given to every new account created.

Config File Values

  • newloginlink

    Setting this to 'Yes' or 'No' determines whether users will be able to create new logins or not. It also determines whether the 'Create New Login' appears on the login screen.

  • adminmail

    This is the email address that reports of new login confirmations are sent to. Setting this to '' switches this off.

  • email_subject

    This is the Subject Line of the emails sent to new users.

  • email_message

    This is the body of the message sent to new users, along with the link to confirm the account.

  • Default User Values

  • max-age

    This is the maximum age of the cookie we use, in seconds. Setting it to zero usually means (slightly browser dependent) the cookie will only endure for that browser session. Common values are 3600=1 hour, 86400=1 day, 604800=1 week. The cookie is reset after every new page access - so this is the maximum time in between visits that the cookie will last. After that, the user will have to login again.

  • editable

    This is whether the user is allowed to change their password, email address etc.

6.2.2   Create or Invite New User

This is where you can create new logins. If you select 'Invite User' then an email will be sent to the address you supply, asking the user to confirm the new login (and telling them their initial password). If you select 'Create New User' then the new login will be created immediately (and no email sent). Admin level should be a number between 0 and 3. For normal users the value should be 0. Obviously user names must be unique. There are a few reserved user names - 'config', 'default', 'temp', 'email'.

6.2.3   Edit or Delete User

From here you can edit or delete any account with a lower admin level than you. Accounts are shown in alphabetical order. You cannot edit or delete yourself, or the main administrator. To edit yourself use the normal 'edit account' option. You can even change the login name of a user here. If you are using the login name to refer to additional files for that user (or database entries) this could cause problems. If you want to be able to plug in a function to calls to this or have the option disabled, then let me know.


7   The Action Value

The action value is used so that your application can tell where in the application the login attempt was made from. When you require a login it is possible that the application will have to restart. If the user isn't already logged in, the 'displaylogin' function is called. This displays the login screen and then exits. When the user does the log in, the application starts again. If you have several places in the application that could have required a login you won't know what the user was trying to do before they had to log in. The action value is a way round this. Any value you pass into the login function will be preserved throughout the whole login process and returned once a successful login has been achieved. This is even true of completely new logins, the action value is stored with the account details and returned once the account is confirmed.

To use it you will need to recognise when a login attempt has been made. logintools uses both a 'login' parameter and the 'action' parameter - you shouldn't use either of these yourself. If your application is called with the 'login' parameter present you know that a login attempt is being made. You should then call the login function. If you passed into the login function a value for the action parameter - you will get this back.

As an illustration, suppose there are three parts of your application that require a user to be logged in. You could achieve this with something like this... (3 chunks of code) :

#.......
action = 'part1'
action, userconfig = login(theform, userdir, thisscript, action)
call_part1(userconfig, theform)

#.......
action = 'part2'
action, userconfig = login(theform, userdir, thisscript, action)
call_part2(userconfig, theform)

#.......
action = 'part1'
action, userconfig = login(theform, userdir, thisscript, action)
call_part3(userconfig, theform)

If the user is already logged in, then the login function returns the userconfig and calls the relevant function - call_part1, call_part2 or call_part3. If the user isn't logged in, then the login screen will be displayed. If the user enters a password then the application is restarted (as is case with any form submission to a CGI). Your application must then pass form submission back to the login function for processing. If the login attempt is successful, then the login function returns the userconfig and the action value that was originally passed in. (action must be a string of course). You can then use this action value to determine what the user was trying to do, and call the correct part of your program. Code to deal with this should go right at the start of the application, and might look like this :

if theform.has_key('login'):
     # only called if we are in the middle of a login process
     action, userconfig = login(theform, userdir, thisscript)
     if action == 'part1':
         call_part1(userconfig, theform)
     elif action == 'part2':
         call_part2(userconfig, theform)
     elif action == 'part3':
         call_part3(userconfig, theform)

If you needed to save any values on the server before calling login (session persistence), you could save the sessionid into action as well.

i.e. something like action = 'part1||' + sessionid, followed by :

if action.startswith('part1'):
    sessionid = action[7:]
    call_part1(userconfig, sessionid)

I don't think logintools plays nicely with other cookies yet (it just prints it's own cookie header). It would be quite easy for me to get it to return a SimpleCookie object instead if you wanted....

The action parameter is passed from page to page to preserve it. It could also be passed as part of a url (a GET request). This means you should limit it's size (1024 bytes is a fair maximum to work to). The longer it is the longer each page will take to load and each form to process.

When a user creates a new account they have to click on a link sent to them by email. The action parameter is saved with their account details and returned to the program when they click on this link.

7.1   formencode and formdecode

You don't have to stick with just preserving single values in the action parameter though. Often you might only want to require a user to be logged in for certain actions. These actions may include a form submission with several values in it. Using a function called 'formencode' you can encode the whole contents of the form into a single string. This string will be returned you in the normal way. You can use a function called 'formdecode' to turn it back into a dictionary of these values. Note that you should still stick with 1024 byte maximum ! This means you should only process file uploads or large form submissions once the user is into a part of the application where they are already logged in. formencode/decode definitely can't handle multipart form data - they can only handle normal single or list values. :

import cgi
from logintools import formencode, formdecode, login
theform = cgi.FieldStorage()
action = None
if not theform.has_key(login):
    action = formencode(theform)
action, userconfig = login(theform, userdir, thisscript, action)
formdict = formdecode(action)

Note that the login function checks for an action parameter in the form first. If one is present it ignores whatever is passed into it. This means that if our user has to login - so a second form submission is received, it will ignore the action value that is passed in on the second submission. If the login is successful, the action that is returned is the original one. formdict is then a dictionary representing the original form passed into the script.

7.2   Action is Used Internally

The nature of logintools is that the user may jump from screen to screen within the login process. (Edit account screen etc). This means two things.

  1. Your application must be able to intercept calls that need to go to the login function. As mentioned before you can do this by checking if your script is called with the login parameter :

    if theform.has_key('login'):
        action, userconfig = login(theform, userdir, thisscript)
    

    (If you're not using the userconfig and action values you could even just call login(theform, userdir, thisscript) which automatically discards the return values).

  2. The action parameter is used internally by the logintools function. This means that it may have strange values when it is passed to your script. Sometimes you might see a url that ends something like &action=EMPTY_VAL_MJF. This is an internal value for action as part of a GET request (form parameters passed in the url). These values should be removed before 'action' is returned to your script. You will only ever get back from action what you put in. Passing in an action value is optional. If you pass nothing in you will either get back '' or None. This means you need to avoid making '' a significant value for action. If this is a problem for anyone, let me know.

8   Dependencies

By default logintools comes with the modules it's dependent on. These are currently : configobj, caseless, listquote, dateutils, dataenc, pathutils and cgiutils. The normal situation is to put all these modules in a folder called modules with login.py and the logintools directory in this folder too.

The home of these modules is http://www.voidspace.org.uk/python/modules.shtml They are all very useful in their own right. These modules include :

  • configobj - easy reading and writing of config files
  • caseless - a case insensitive dictionary, list and sort function
  • dataenc - timestamp and encode values for including in form fields and cookies
  • dateutils - functions for dealing with dates, particularly useful is the date to julian day number
  • listquote - reading lists and quoting/unquoting elements
  • cgiutils - a few standard functions for use with CGI (including form handling and sending email)
  • pathutils - basic file and path handling functions.

logintools will only work on Python 2.2 or more recent.


9   Keyword Lists in templates

The userconfig file make a very logical place to store user preferences etc. As I discussed in Extending the User Config File, this means that you may need to provide a web interface to amending values in the userconfig file. This would be for both your users and for admin purposes. It is made more complicated by :

  • You wouldn't want to edit every value
  • Some values should only be editable by admin
  • Some values may be more appropriately edited from a checkbox or drop down list

It is perfectly possible to develop a schema that implements all this, but it's not straightforward. I will only implement this fully if I need it, or if someone else asks for it and is willing to help me test it. I have already implemented a start of this in the user sign up section. (untested so far, but it should work.)

It works like this. In the template file newlogin_nojs.txt is the form used to get new logins. By default it only collects the username, real name, password and email. You can extend this form to include extra values with text inputs. These extra values are just put straight into the user config file. Easy hey ! There's a bit more to it though. When a validation error occurs the form is reprinted for the error to be corrected. This could be something like the email address being invalid, the passwords not matching, or the username already existing. When this happens it is much more friendly if the values the user has just entered are kept in the form (rather than them losing everything they have typed).

To do this, logintools needs to know what fields you are using and where it should put these values. For every additional input field in your form you need to add value="**fieldname**" . You should also include somewhere in the page a comment containing a list of all the fields to be filled in. The list should look as follows <!-- **keynamelist** fieldname1, fieldname2, fieldname3 -->. The key thing is that it starts <!-- **keynamelist** so that logintools can find it.

This minimalist system has various flaws. The first time it is run it ought to fill in missing values from 'default.ini'. There is no way of making any of these new keys required keys. More importantly you can't yet edit/view these details in the admin edit account screen, or edit default user or in create/invite new user.

10   Functions Overview

We've already looked at the 'login' function and the 'isloggedin' function, and also sneaked a glance at 'createuser'. There are one or two other functions that can be imported from logintools and may be useful. The full list of functions that can be imported is :

So long as your version of python respects __all__, from logintools import * ought to be the same as from logintools import login, isloggedin, createuser, logout, checklogin, displaylogin.

10.1   login

This is the main logintools function. Every script that uses logintools ought to include a chunk of code, similar to the following, very near the start :

theform = cgi.FieldStorage()
if theform.has_key('login'):
    action, userconfig = login(theform, userdir)

If you are requiring your users to login before they can access any of your application, you can probably just start your script with something like :

theform = cgi.FieldStorage()
action, userconfig = login(theform, userdir)

login also takes two optional keyword arguments : login(theform, userdir, thisscript=None, action=None). 'action' is a value propagated through the login process. You can use it to tell where the call to login was made from. See the section The Action Value. 'thisscript' is the script that you want to be called from actions within the login process. It defaults to the cgi script that was called by the user.

If the person who called the script doesn't have a valid cookie (i.e. isn't logged in) then login calls the displaylogin function and exits. If there is a logged in user, then login returns the action value (or '' or None) and the userconfig file. The userconfig file is a ConfigObj instance that contains the values associated with the currently logged in user.

10.2   isloggedin

This function can be used to tell if there is a currently logged in user or not. Unlike 'login' it doesn't display the login screen (or exit) if the answer is no. This is useful for providing different options to logged in users. The syntax is :

test = isloggedin(userdir)
if test != False:
    userconfig, cookieheader = test

isloggedin returns False if no user is logged in. It returns a (userconfig, cookieheader) tuple if one is. The cookieheader can be printed (sensible if you never call login - it updates the time on the cookie).

10.3   createuser

If you need to be able to create accounts from your program you can import and use the 'createuser' function. This doesn't check first if the username already exists ! (So you could use it just to change values in an existing user) :

from logintools import createuser
createuser(userdir, realname, username, email, password)

10.4   logout

This function just returns a cookie header line. If this is printed (as an http header, before the contents of your page), then it cancels the users cookie - effectively logging them out.

cookieheader = logout(userdir)
print cookieheader

10.5   checklogin

This is the function called by the login function. It either returns a (userconfig, action, updatedcookieheader) tuple or it displays the login screen and exits. It doesn't do a lot of the other stuff that the login screen does though, like handling the admin screen or the edit account screen. It also doesn't print the updated cookie header, but returns it to you. It doesn't require a copy of the form, since all it is doing is checking the cookie header for validity.

action, userconfig, cookieheader = checklogin(userdir, thisscript=None, action=None)

10.6   displaylogin

This function simply displays the login screen and then exits. Nice and simple.

displaylogin(userdir, thisscript=None, action=None)

11   Template Files and Values

It is expected that the templates in the 'logintemplates' directory will be edited. This allows you to change the look of the login and admin pages so that they fit in with the rest of your application. In each template there are certain values that need to remain. Some of them are for automatically generated links, and can optionally be omitted. The rest fill in values in the forms that must be submitted to the various login functions. This sections list the template files and the values in there. All the optional ones are recommended, but who am I to tell you what to do ?

In each page various values are replaced with the actual parameter being used. The values generally look like this : **some name** - using any of these in the page will cause them to be replaced as well. If the template contains a form, then any input fields etc. must be included in whatever you use. If you understand HTML forms then it's straightforward - if you don't, then it's best not to mess with anything between the <form...> </form> tags.

11.1   login_page.html

This is the login page. This is the page displayed by the login function, if there is no user currently logged in. (and always displayed by displaylogin).

  • **login form** This is replaced with the login form. This is not optional.

If you didn't want the new login link to appear here, but still wanted new logins enabled for the application, you could remove everything from <!-- **commstart** to **commend** -->.

  • <!-- **commstart** This is a comment start. It is removed if your application allows users to sign up for new logins.
  • **new login link** This is replaced with a link to create new logins.
  • **commend** --> This is the comment end. It is also removed if new logins are allowed.

11.2   login_nojs.txt

This is the standard form that is put into the login page. If you edit this, you must leave all the input fields within the form. Changing any part of the form will affect logintools.

  • **script** The main script to send the form submissions to. Not optional.
  • <!-- **action** --> Replaced with the action parameter. Not optional.

11.3   newlogin_page.html

This is the page that is displayed when someone wants to sign up for a new account.

  • <!-- **message** --> This is used to display messages to the user if any errors occur. Not optional.
  • **new login form** This is replaced with the new login form. Not optional.

11.4   newlogin_nojs.txt

This is the standard form that goes into the newlogin page.

  • **script** The main script to send the form submissions to. Not optional.
  • **realname** This is replaced with the realname in the event of error. Not optional.
  • **username** This is replaced with the login name in the event of error. Not optional.
  • **email** This is replaced with the email in the event of error. Not optional.

11.5   login_done.html

This is the page displayed when the new login process is completed.

  • **this script** A link back to the main script. Optional

11.6   edacc_page.html

This is the page that is displayed when the user edits their account.

  • <!-- **message** --> This is used to display messages to the user if any errors occur. Not optional.
  • **created on** Shows when the account was created. Optional
  • **num logins** Shows how many times this user has logged in. Optional
  • **num used** Shows how many protected pages the user has accessed. Optional
  • **last used** Shows when the account was last used - will always be recent ! Optional
  • **this script** Link back to the main script. Optional
  • **edit account form** Replaced with the edit account form. Not optional.

11.7   editform_nojs.txt

This is the form that goes into the user edit account page.

  • **script** The main script to send the form submissions to. Not optional.
  • **realname** This is replaced with the realname in the event of error. Not optional.
  • **email** This is replaced with the email in the event of error. Not optional.
  • <!-- **action** --> Replaced with the action parameter. Not optional.

11.8   admin_page.html

This is the main template for the admin pages. It is used for all the pages accessed through the admin menu. The contents of the page is inserted where the **admin** marker appears. The differing contents for the various pages are based on the text files that start 'admin_'.

  • **this script** Link back to the main script. Optional
  • **admin menu** Link back to the admin menu. Optional
  • <!-- **message** --> This is used to display messages to the user if any errors occur. Not optional.
  • **admin** The actual admin page being viewed. Not optional

11.9   admin_menu.txt

This is the main admin menu screen.

  • **invite** Link to invite/create new users. Not optional.
  • **edit users** Link to edit/delete users. Not optional.
  • **edit config** Link to edit the config file/default user. Not optional.

11.10   admin_eduser.txt

This is the header of the page for editing and deleting users. The actual table for each user is hardwired into admin.py. It could probably be broken out into a template if anyone wanted to edit it.

  • **account table** Replaced with the table of accounts. Not optional.

11.11   admin_config.txt

This is the template used when you are in the edit config file/default user page.

  • **script** The main script to send the form submissions to. Not optional.
  • **action** Replaced with the action parameter. Not optional.

The rest of the values are filled in with entries from the default user and the config file. None of these are optional.

  • **loginlink**
  • **adminmail**
  • **email subject**
  • **email message**
  • **maxage**
  • **editable**

11.12   admin_invite.txt

This is the template used when you are in the invite/create new user screen.

  • **script** The main script to send the form submissions to. Not optional.
  • **action** Replaced with the action parameter. Not optional.

The following values are filled in if you make a mistake in your form (email address invalid, user already exists etc). They ensure that the values you typed first time are preserved. The exception is the password, which is randomly generated. None of them are optional.

  • **create1**
  • **create2**
  • **realname**
  • **username**
  • **email**
  • **adminlev**
  • **pass1**
  • **pass2**

12   TODO

This TODO list is the list of features/fixes that I am likely to actually get round to at some point. They aren't in order of priority, but some are obviously more important than others. If any particularly matter to you, or you have suggestions for things that ought to be on the list, then let me know. If you wanted to tackle any yourself I can give you lots of help. Some of the items can already be done, either through the templates or just by writing external functions - but they ought to be built into logintools.

13   WISH LIST

This is the part of the TODO list that I will only work on if time permits or if someone really pesters me for it. If you really want anything from this list you'll have to let me know and I'll have to try and move it up the priority list. As above, if you wanted to tackle any yourself I can give you lots of help.

14   ISSUES

These 'issues' are things that don't work as they should. Some of them are bugs, some of them have solutions, and some aren't very important but are worth being aware of.

15   Changelog

15.1   2005/11/02 Version 0.6.2

Major bugfix in the email functions oops. The function calls weren't updated to account for the changes in Pythonutils 0.2.3. Embarassed

15.2   2005/10/11 Version 0.6.1

Bugfix in newlogin.py

15.3   2005/09/21 Version 0.6.0

Changes to use the Pythonutils 0.2.2 modules.

15.4   2004/11/29 Version 0.5.0a

First release into the wild. Packaged along with (and built for) Jalopy. Works well.

16   License

logintools is licensed under the BSD license. This is a very unrestrictive license - but it comes with the usual disclaimer. This is free software - test it, break it, just don't blame me if it eats your data ! (If it does though, let me know and I'll fix it so that it doesn't happen to anyone else Smile

Copyright (c) 2003-2005, Michael Foord
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:


    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

    * Neither the name of Michael Foord nor the name of Voidspace 
      may be used to endorse or promote products derived from this
      software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

You should also be able to find a copy of this license at : BSD License

If you use this program, please help Sponsor Voidspace, to assist with the costs of hosting Voidspace. Even $1 or $2 is helpful !




Certified Open Source