Authentication

In order to use RBAC, users need to be identified. This means that they need to register (or be registered) and log in.

Auth provides multiple login methods. The default one consists of identifying users based on the local auth_user table. Alternatively, it can log in users against third-party authentication systems and single sign on providers such as Google, PAM, LDAP, Facebook, LinkedIn, Dropbox, OpenID, OAuth, etc..

To start using Auth, you need at least this code in a model file, which is also provided with the web2py “welcome” application and assumes a db connection object:

  1. from gluon.tools import Auth
  2. auth = Auth(db)
  3. auth.define_tables(username=False, signature=False)

By default, web2py uses email for login. If instead you want to log in using username set auth.define_tables(username=True)

Setting signature=True adds user and date stamping to auth tables, to track modifications.

Auth has an optional secure=True argument, which will force authenticated pages to go over HTTPS.

By default, Auth protects logins against cross-site request forgeries (CSRF). This is actually provided by web2py’s standard CSRF protection whenever forms are generated in a session. However, under some circumstances, the overhead of creating a session for login,password request and reset attempts may be undesirable. DOS attacks are theoretically possible. CSRF protection can be disabled for Auth forms (as of v 2.6):

  1. Auth = Auth(..., csrf_prevention = False)

Note that doing this purely to avoid session overload on a busy site is not recommended because of the introduced security risk. Instead, see the Deployment chapter for advice on reducing session overheads.

The password field of the db.auth_user table defaults to a CRYPT validator, which needs an hmac_key. On legacy web2py applications you may see an extra argument passed to the Auth constructor: hmac_key = Auth.get_or_create_key(). The latter is a function that read the HMAC key from a file “private/auth.key” within the application folder. If the file does not exist it creates a random hmac_key. If multiple apps share the same auth database, make sure they also use the same hmac_key. This is no longer necessary for new applications since passwords are salted with an individual random salt.

If multiple apps share the same auth database you may want to disable migrations: auth.define_tables(migrate=False).

To expose Auth, you also need the following function in a controller (for example in “default.py”):

  1. def user(): return dict(form=auth())

The auth object and the user action are already defined in the scaffolding application.

web2py also includes a sample view “welcome/views/default/user.html” to render this function properly that looks like this:

  1. {{extend 'layout.html'}}
  2. <h2>{{=T( request.args(0).replace('_', ' ').capitalize() )}}</h2>
  3. <div id="web2py_user_form">
  4. {{=form}}
  5. {{if request.args(0)=='login':}}
  6. {{if not 'register' in auth.settings.actions_disabled:}}
  7. <br/><a href="{{=URL(args='register')}}">register</a>
  8. {{pass}}
  9. {{if not 'request_reset_password' in auth.settings.actions_disabled:}}
  10. <br/>
  11. <a href="{{=URL(args='request_reset_password')}}">lost password</a>
  12. {{pass}}
  13. {{pass}}
  14. </div>

Notice that this function simply displays a form and therefore it can be customized using normal custom form syntax. The only caveat is that the form displayed by form=auth() depends on request.args(0); therefore, if you replace the default auth() login form with a custom login form, you may need an if statement like this in the view:

  1. {{if request.args(0)=='login':}}...custom login form...{{pass}}

The controller above exposes multiple actions:

  1. http://.../[app]/default/user/register
  2. http://.../[app]/default/user/login
  3. http://.../[app]/default/user/logout
  4. http://.../[app]/default/user/profile
  5. http://.../[app]/default/user/change_password
  6. http://.../[app]/default/user/verify_email
  7. http://.../[app]/default/user/retrieve_username
  8. http://.../[app]/default/user/request_reset_password
  9. http://.../[app]/default/user/reset_password
  10. http://.../[app]/default/user/impersonate
  11. http://.../[app]/default/user/groups
  12. http://.../[app]/default/user/not_authorized
  • register allows users to register. It is integrated with CAPTCHA, although this is disabled by default. This is also integrated with a client-side entropy calculator defined in “web2py.js”. The calculator indicates the strength of the new password. You can use the IS_STRONG validator to prevent web2py from accepting weak passwords.
  • login allows users who are registered to log in (if the registration is verified or does not require verification, if it has been approved or does not require approval, and if it has not been blocked).
  • logout does what you would expect but also, as the other methods, logs the event and can be used to trigger some event.
  • profile allows users to edit their profile, i.e. the content of the auth_user table. Notice that this table does not have a fixed structure and can be customized.
  • change_password allows users to change their password in a fail-safe way.
  • verify_email. If email verification is turned on, then visitors, upon registration, receive an email with a link to verify their email information. The link points to this action.
  • retrieve_username. By default, Auth uses email and password for login, but it can, optionally, use username instead of email. In this latter case, if a user forgets his/her username, the retrieve_username method allows the user to type the email address and retrieve the username by email.
  • request_reset_password. Allows users who forgot their password to request a new password. They will get a confirmation email pointing to reset_password.
  • impersonate allows a user to “impersonate” another user. This is important for debugging and for support purposes. request.args[0] is the id of the user to be impersonated. This is only allowed if the logged in user has_permission('impersonate', db.auth_user, user_id). You can use auth.is_impersonating() to check is the current user is impersonating somebody else.
  • groups lists the groups of which the current logged in user is a member.
  • not_authorized displays an error message when the visitor tried to do something that he/she is not authorized to do
  • navbar is a helper that generates a bar with login/register/etc. links.

Logout, profile, change_password, impersonate, and groups require login.

By default they are all exposed, but it is possible to restrict access to only some of these actions.

All of the methods above can be extended or replaced by subclassing Auth.

All of the methods above can be used in separate actions. For example:

  1. def mylogin(): return dict(form=auth.login())
  2. def myregister(): return dict(form=auth.register())
  3. def myprofile(): return dict(form=auth.profile())
  4. ...

To restrict access to functions to only logged in visitors, decorate the function as in the following example

  1. @auth.requires_login()
  2. def hello():
  3. return dict(message='hello %(first_name)s' % auth.user)

Any function can be decorated, not just exposed actions. Of course this is still only a very simple example of access control. More complex examples will be discussed later.

auth.user_groups.

auth.user contains a copy of the db.auth_user records for the current logged in user or None otherwise. There is also a auth.user_id which is the same as auth.user.id (i.e. the id of the current logger in user) or None. Similarly, auth.user_groups contains a dictionary where each key is the id of a group of with the current logged in user is member of, the value is the corresponding group role.

The auth.requires_login() decorator as well as the other auth.requires_* decorators take an optional otherwise argument. It can be set to a string where to redirect the user if registration files or to a callable object. It is called if registration fails.

Restrictions on registration

If you want to allow visitors to register but not to log in until registration has been approved by the administrator:

  1. auth.settings.registration_requires_approval = True

You can approve a registration via the appadmin interface. Look into the table auth_user. Pending registrations have a registration_key field set to “pending”. A registration is approved when this field is set to blank.

Via the appadmin interface, you can also block a user from logging in. Locate the user in the table auth_user and set the registration_key to “blocked”. “blocked” users are not allowed to log in. Notice that this will prevent a visitor from logging in but it will not force a visitor who is already logged in to log out. The word “disabled” may be used instead of “blocked” if preferred, with exactly the same behavior.

You can also block access to the “register” page completely with this statement:

  1. auth.settings.actions_disabled.append('register')

If you want to allow people to register and automatically log them in after registration but still want to send an email for verification so that they cannot login again after logout, unless they completed the instructions in the email, you can accomplish it as follows:

  1. auth.settings.registration_requires_verification = True
  2. auth.settings.login_after_registration = True

Other methods of Auth can be restricted in the same way.

Integration with OpenID, Facebook, etc.

You can use the web2py Role Base Access Control and authenticate with other services like OpenID, Facebook, LinkedIn, Google, Dropbox, MySpace, Flickr, etc. The easiest way is to use Janrain Engage (formerly RPX) (Janrain.com).

Dropbox is discussed as a special case in Chapter 14 since it allows more than just login, it also provides storage services for the logged in users.

Janrain Engage is a service that provides middleware authentication. You can register with Janrain.com, register a domain (the name of your app) and set of URLs you will be using, and they will provide you with an API key.

Now edit the model of your web2py application and place the following lines somewhere after the definition of the auth object :

  1. from gluon.contrib.login_methods.rpx_account import RPXAccount
  2. auth.settings.actions_disabled=['register', 'change_password', 'request_reset_password']
  3. auth.settings.login_form = RPXAccount(request,
  4. api_key='...',
  5. domain='...',
  6. url = "http://your-external-address/%s/default/user/login" % request.application)

The first line imports the new login method, the second line disables local registration, and the third line asks web2py to use the RPX login method. You must insert your own api_key provided by Janrain.com, the domain you choose upon registration and the external url of your login page. To obtain then login at janrain.com, then go to [Deployment][Application Settings]. On the right side there is the “Application Info”, The api_key is called “API Key (Secret)”.

The domain is the “Application Domain” without leading “https://“ and without the trailing “.rpxnow.com/“ For example: if you have registered a website as “secure.mywebsite.org”, Janrain turns it to the Application Domain “https://secure-mywebsite.rpxnow.com“.

image

When a new user logins for the first time, web2py creates a new db.auth_user record associated to the user. It will use the registration_id field to store a unique id for the user. Most authentication methods will also provide a username, email, first_name and last_name but that is not guaranteed. Which fields are provided depends on the login method selected by the user. If the same user logs in twice using different authentication mechanisms (for example once with OpenID and once with Facebook), Janrain may not recognize his/her as the same user and issue different registration_id.

You can customize the mapping between the data provided by Janrain and the data stored in db.auth_user. Here is an example for Facebook:

  1. auth.settings.login_form.mappings.Facebook = lambda profile: dict(registration_id = profile["identifier"],
  2. username = profile["preferredUsername"],
  3. email = profile["email"],
  4. first_name = profile["name"]["givenName"],
  5. last_name = profile["name"]["familyName"])

The keys in the dictionary are fields in db.auth_user and the values are data entries in the profile object provided by Janrain. Look at the online Janrain documentation for details on the latter.

Janrain will also keep statistics about your users’ login.

This login form is fully integrated with web2py Role Based Access Control and you can still create groups, make users members of groups, assign permissions, block users, etc.

Janrain’s free Basic service allows up to 2500 unique registered users to sign in annually. Accommodating more users requires an upgrade to one of their paid service tiers.

If you prefer not to use Janrain and want to use a different login method (LDAP, PAM, Google, OpenID, OAuth/Facebook, LinkedIn, etc.) you can do so. The API to do so is described later in the chapter.

CAPTCHA and reCAPTCHA

To prevent spammers and bots registering on your site, you may require a registration CAPTCHA. web2py supports reCAPTCHA[recaptcha] out of the box. This is because reCAPTCHA is very well designed, free, accessible (it can read the words to the visitors), easy to set up, and does not require installing any third-party libraries.

This is what you need to do to use reCAPTCHA:

  • Register with reCAPTCHA[recaptcha] V2 and obtain a (PUBLIC_KEY, PRIVATE_KEY) couple for your account. These are just two strings.
  • Append the following code to your model after the auth object is defined:
  1. from gluon.tools import Recaptcha2
  2. auth.settings.captcha = Recaptcha2(request,
  3. 'PUBLIC_KEY', 'PRIVATE_KEY')

reCAPTCHA may not work if you access the web site as ‘localhost’ or ‘127.0.0.1’, because it is registered to work with publicly visible web sites only, unless you add them to the domains in the reCAPTCHA admin panel.

The Recaptcha2 constructor takes some optional arguments:

  1. Recaptcha2(..., error_message='invalid', label='Verify:', options={})

There is an experimental argument, ajax=True, which uses the ajax API to recaptcha. It can be used with any recaptcha, but it was specifically added to allow recpatcha fields to work in LOAD forms (see Chapter 12 for more about LOAD, which allows web2py to ‘plugin’ components of a page with ajax ). It’s experimental because it may be replaced with automatic detection of when ajax is required.

options may be a configuration dictionary, e.g. options={theme: 'white', size: 'normal'}

More details: reCAPTCHA[recaptchagoogle] and configuration.

If you do not want to use reCAPTCHA, look into the definition of the Recaptcha2 class in “gluon/tools.py”, since it is easy to use other CAPTCHA systems.

Notice that Recaptcha2 is just a helper that extends DIV. It generates a dummy field that validates using the reCaptcha service and, therefore, it can be used in any form, including used defined FORMs:

  1. form = FORM(INPUT(...), Recaptcha2(...), INPUT(_type='submit'))

You can use it in all types of SQLFORM by injection:

  1. form = SQLFORM(...) or SQLFORM.factory(...)
  2. form.insert(-1, TR('', Recaptcha2(...), ''))

Customizing Auth

The call to

  1. auth.define_tables()

defines all Auth tables that have not been defined already. This means that if you wish to do so, you can define your own auth_user table.

There are a number of ways to customize auth. The simplest way is to add extra fields:

  1. ## after auth = Auth(db)
  2. auth.settings.extra_fields['auth_user']= [
  3. Field('address'),
  4. Field('city'),
  5. Field('zip'),
  6. Field('phone')]
  7. ## before auth.define_tables(username=True)

You can declare extra fields not just for table “auth_user” but also for other “auth_“ tables. Using extra_fields is the recommended way as it will not break any internal mechanism.

Another way to do this, although not really recommended, consists of defining your auth tables yourself. If a table is declared before auth.define_tables() it is used instead of the default one. Here is how to do it:

  1. ## after auth = Auth(db)
  2. db.define_table(
  3. auth.settings.table_user_name,
  4. Field('first_name', length=128, default=''),
  5. Field('last_name', length=128, default=''),
  6. Field('email', length=128, default='', unique=True), # required
  7. Field('password', 'password', length=512, # required
  8. readable=False, label='Password'),
  9. Field('address'),
  10. Field('city'),
  11. Field('zip'),
  12. Field('phone'),
  13. Field('registration_key', length=512, # required
  14. writable=False, readable=False, default=''),
  15. Field('reset_password_key', length=512, # required
  16. writable=False, readable=False, default=''),
  17. Field('registration_id', length=512, # required
  18. writable=False, readable=False, default=''))
  19. ## do not forget validators
  20. custom_auth_table = db[auth.settings.table_user_name] # get the custom_auth_table
  21. custom_auth_table.first_name.requires = IS_NOT_EMPTY(error_message=auth.messages.is_empty)
  22. custom_auth_table.last_name.requires = IS_NOT_EMPTY(error_message=auth.messages.is_empty)
  23. custom_auth_table.password.requires = [IS_STRONG(), CRYPT()]
  24. custom_auth_table.email.requires = [
  25. IS_EMAIL(error_message=auth.messages.invalid_email),
  26. IS_NOT_IN_DB(db, custom_auth_table.email)]
  27. auth.settings.table_user = custom_auth_table # tell auth to use custom_auth_table
  28. ## before auth.define_tables()

You can add any field you wish, and you can change validators but you cannot remove the fields marked as “required” in this example.

It is important to make “password”, “registration_key”, “reset_password_key” and “registration_id” fields readable=False and writable=False, since a visitor must not be allowed to tamper with them.

If you add a field called “username”, it will be used in place of “email” for login. If you do, you will need to add a validator as well:

  1. auth_table.username.requires = IS_NOT_IN_DB(db, auth_table.username)

Note that Auth caches the logged in user in the session and that’s what you get in auth.user, so you need to clear the sessions for the extra fields changes to be reflected in it.

Renaming Auth tables

[renaming_auth_tables]

The actual names of the Auth tables are stored in

  1. auth.settings.table_user_name = 'auth_user'
  2. auth.settings.table_group_name = 'auth_group'
  3. auth.settings.table_membership_name = 'auth_membership'
  4. auth.settings.table_permission_name = 'auth_permission'
  5. auth.settings.table_event_name = 'auth_event'

The names of the table can be changed by reassigning the above variables after the auth object is defined and before the Auth tables are defined. For example:

  1. auth = Auth(db)
  2. auth.settings.table_user_name = 'person'
  3. #...
  4. auth.define_tables()

The actual tables can also be referenced, independently of their actual names, by

  1. auth.settings.table_user
  2. auth.settings.table_group
  3. auth.settings.table_membership
  4. auth.settings.table_permission
  5. auth.settings.table_event

Note: auth.signature gets defined when Auth is initialized, which is before you have set the custom table names. To avoid this do:

  1. auth = Auth(db, signature=False)

In that case, auth.signature will instead be defined when you call auth.define_tables(), by which point the custom tables names will already be set.

Other login methods and login forms

Auth provides multiple login methods and hooks to create new login methods. Each supported login method corresponds to a file in the folder

  1. gluon/contrib/login_methods/

Refer to the documentation in the files themselves for each login method, but here are some examples.

First of all, we need to make a distinction between two types of alternate login methods:

  • login methods that use a web2py login form (although the credentials are verified outside web2py). An example is LDAP.
  • login methods that require an external single-sign-on form (an example is Google and Facebook).

In the latter case, web2py never gets the login credentials, only a login token issued by the service provider. The token is stored in db.auth_user.registration_id.

Let’s consider examples of the first case:

Basic

Let’s say you have an authentication service, for example at the url

  1. https://basic.example.com

that accepts basic access authentication. That means the server accepts HTTP requests with a header of the form:

  1. GET /index.html HTTP/1.0
  2. Host: basic.example.com
  3. Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

where the latter string is the base64 encoding of the string username:password. The service responds 200 OK if the user is authorized and 400, 401, 402, 403 or 404 otherwise.

You want to enter username and password using the standard Auth login form and verify the credentials against such a service. All you need to do is add the following code to your application

  1. from gluon.contrib.login_methods.basic_auth import basic_auth
  2. auth.settings.login_methods.append(
  3. basic_auth('https://basic.example.com'))

Notice that auth.settings.login_methods is a list of authentication methods that are executed sequentially. By default it is set to

  1. auth.settings.login_methods = [auth]

When an alternate method is appended, for example basic_auth, Auth first tries to log in the visitor based on the content of auth_user, and when this fails, it tries the next method in the list. If a method succeeds in logging in the visitor, and if auth.settings.login_methods[0]==auth, Auth takes the following actions:

  • if the user does not exist in auth_user, a new user is created and the username/email and passwords are stored.
  • if the user does exist in auth_user but the new accepted password does not match the old stored password, the old password is replaced with the new one (notice that passwords are always stored hashed unless specified otherwise).

If you do not wish to store the new password in auth_user, then it is sufficient to change the order of login methods, or remove auth from the list. For example:

  1. from gluon.contrib.login_methods.basic_auth import basic_auth
  2. auth.settings.login_methods = [basic_auth('https://basic.example.com')]

The same applies for any other login method described here.

SMTP and Gmail

You can verify login credentials using a remote SMTP server, for example Gmail; i.e., you log the user in if the email and password they provide are valid credentials to access the Gmail SMTP server (smtp.gmail.com:587). All that is needed is the following code:

  1. from gluon.contrib.login_methods.email_auth import email_auth
  2. auth.settings.login_methods.append(
  3. email_auth("smtp.gmail.com:587", "@gmail.com"))

The first argument of email_auth is the address:port of the SMTP server. The second argument is the email domain.

This works with any SMTP server that requires TLS authentication.

PAM

Authentication using Pluggable Authentication Modules (PAM) works as in the previous cases. It allows web2py to authenticate users using the operating system accounts:

  1. from gluon.contrib.login_methods.pam_auth import pam_auth
  2. auth.settings.login_methods.append(pam_auth())
LDAP

Authentication using LDAP works very much as in the previous cases.

To use LDAP login with MS Active Directory:

  1. from gluon.contrib.login_methods.ldap_auth import ldap_auth
  2. auth.settings.login_methods.append(ldap_auth(mode='ad',
  3. server='my.domain.controller',
  4. base_dn='ou=Users,dc=domain,dc=com'))

To use LDAP login with Lotus Notes and Domino:

  1. auth.settings.login_methods.append(ldap_auth(mode='domino',
  2. server='my.domino.server'))

To use LDAP login with OpenLDAP (with UID):

  1. auth.settings.login_methods.append(ldap_auth(server='my.ldap.server',
  2. base_dn='ou=Users,dc=domain,dc=com'))

To use LDAP login with OpenLDAP (with CN):

  1. auth.settings.login_methods.append(ldap_auth(mode='cn',
  2. server='my.ldap.server', base_dn='ou=Users,dc=domain,dc=com'))

There are additional parameters to let web2py

  • read additional data like the username from LDAP
  • implement group control
  • restrict login access.

See the documentation of ldap_auth in web2py/gluon/contrib/login_methods/ldap_auth.py.

Google App Engine

Authentication using Google when running on Google App Engine requires skipping the web2py login form, being redirected to the Google login page, and back upon success. Because the behavior is different than in the previous examples, the API is a little different.

  1. from gluon.contrib.login_methods.gae_google_login import GaeGoogleAccount
  2. auth.settings.login_form = GaeGoogleAccount()
OpenID

We have previously discussed integration with Janrain (which has OpenID support) and that is the easiest way to use OpenID. Yet sometimes you do not want to rely on a third party service and you want to access the OpenID provider directly from the consumer (your app).

Here is an example:

  1. from gluon.contrib.login_methods.openid_auth import OpenIDAuth
  2. auth.settings.login_form = OpenIDAuth(auth)

OpenIDAuth requires the python-openid module to be installed separately. Under the hood, this login method defines the following table:

  1. db.define_table('alt_logins',
  2. Field('username', length=512, default=''),
  3. Field('type', length =128, default='openid', readable=False),
  4. Field('user', self.table_user, readable=False))

which stores the openid usernames for each user. If you want to display the openids for the current logged in user:

  1. {{=auth.settings.login_form.list_user_openids()}}
OAuth2.0

We have previously discussed integration with Janrain, yet sometimes you do not want to rely on a third party service and you want to access a OAuth2.0 provider directly; for example, Facebook, Linkedin, Twitter, Google all of them provide an OAuth2.0 authentication service. web2py handles the OAuth2.0 flow transparently so that a user can be verified against any configured OAuth2.0 provider during login. Other than authentication an OAuth2.0 provider can grant to any web2py application access to user resources with restricted access thought a proprietary API. Google, Twitter, Facebook and so on, all have APIs that can be easily accessed by a web2py application.

It must be underlined that OAuth2.0 is limited only to authentication and authorization (for instance CAS has more functionalities), this means that each OAuth2.0 provider has a different way to receive a unique id from their user database through one of their APIs. Specific methods are well explained on respective provider documentation, they usually consist in a very simple REST call. This is why for each OAuth2.0 provider there is the need to write a few lines of code.

Before writing any instructions in the application model a first step is needed for any provider: registering a new application; this is usually done on provider’s site and is explained in provider’s documentation.

There are a few things that needs to be known once there is the need to add a new OAuth2.0 provider to your application: 1. the Authorization URI; 2. the Token request URI; 3. the application identification token and secret received upon registration of the new application; 4. the permissions that the provider must grant to the web2py application, i.e. the “scope” (see the provider’s documentation); 5. the API call to receive a UID of the authenticating user, as explained on providers documentation.

Point 1 to 4 are used to initialize the authorization endpoint used by web2py to communicate with the OAuth2.0 provider. The unique id is retrieved by web2py with a call to the get_user() method when needed during the login flow; this is where the API call of point 5 is needed.

These are the essential modification that need to be done in your model: a. import OAuthAccount class; b. define a derived OAuthClass implementation; c. override __init__() method of that class; d. override get_user() method of that class. e. instantiate the class with the data of points 1-4 of the above list;

Once the class is instantiated, and the user is authenticated, the web2py application can access the API of the provider any time by using the OAuth2.0 access token by calling the accessToken() method of that class.

What follows is an example of what can be used with Facebook. This is a basic example using Facebook Graph API, remind that, by writing a proper get_user() method, many different things can be done. The example shows how the OAuth2.0 access token can be used when calling the remote API of the provider.

First of all you must install the Facebook Python SDK.

Second, you need the following code in your model:

  1. ## Define oauth application id and secret.
  2. FB_CLIENT_ID='xxx'
  3. FB_CLIENT_SECRET="yyyy"
  4. ## import required modules
  5. try:
  6. import json
  7. except ImportError:
  8. from gluon.contrib import simplejson as json
  9. from facebook import GraphAPI, GraphAPIError
  10. from gluon.contrib.login_methods.oauth20_account import OAuthAccount
  11. ## extend the OAUthAccount class
  12. class FaceBookAccount(OAuthAccount):
  13. """OAuth impl for FaceBook"""
  14. AUTH_URL="https://graph.facebook.com/oauth/authorize"
  15. TOKEN_URL="https://graph.facebook.com/oauth/access_token"
  16. def __init__(self):
  17. OAuthAccount.__init__(self, None, FB_CLIENT_ID, FB_CLIENT_SECRET,
  18. self.AUTH_URL, self.TOKEN_URL,
  19. scope='email,user_about_me,user_activities, user_birthday, user_education_history, user_groups, user_hometown, user_interests, user_likes, user_location, user_relationships, user_relationship_details, user_religion_politics, user_subscriptions, user_work_history, user_photos, user_status, user_videos, publish_actions, friends_hometown, friends_location,friends_photos',
  20. state="auth_provider=facebook",
  21. display='popup')
  22. self.graph = None
  23. def get_user(self):
  24. '''Returns the user using the Graph API.
  25. '''
  26. if not self.accessToken():
  27. return None
  28. if not self.graph:
  29. self.graph = GraphAPI((self.accessToken()))
  30. user = None
  31. try:
  32. user = self.graph.get_object("me")
  33. except GraphAPIError, e:
  34. session.token = None
  35. self.graph = None
  36. if user:
  37. if not user.has_key('username'):
  38. username = user['id']
  39. else:
  40. username = user['username']
  41. if not user.has_key('email'):
  42. email = '%s.fakemail' %(user['id'])
  43. else:
  44. email = user['email']
  45. return dict(first_name = user['first_name'],
  46. last_name = user['last_name'],
  47. username = username,
  48. email = '%s' %(email) )
  49. ## use the above class to build a new login form
  50. auth.settings.login_form=FaceBookAccount()
LinkedIn

We have previously discussed integration with Janrain (which has LinkedIn support) and that is the easiest way to use OAuth. Yet sometime you do not want to rely on a third party service or you may want to access LinkedIn directly to get more information than Janrain provides.

Here is an example:

  1. from gluon.contrib.login_methods.linkedin_account import LinkedInAccount
  2. auth.settings.login_form=LinkedInAccount(request,KEY,SECRET,RETURN_URL)

LinkedInAccount requires the “python-linkedin” module installed separately.

X509

You can also login by passing to the page an x509 certificate and your credential will be extracted from the certificate. This requires M2Crypto installed from

  1. http://chandlerproject.org/bin/view/Projects/MeTooCrypto

Once you have M2Cryption installed you can do:

  1. from gluon.contrib.login_methods.x509_auth import X509Account
  2. auth.settings.actions_disabled=['register', 'change_password', 'request_reset_password']
  3. auth.settings.login_form = X509Account()

You can now authenticate into web2py passing your x509 certificate. How to do this is browser-dependent, but probably you are more likely to use certificates for web services. In this case you can use for example cURL to try out your authentication:

  1. curl -d "firstName=John&lastName=Smith" -G -v --key private.key --cert server.crt https://example/app/default/user/profile

This works out of the box with Rocket (the web2py built-in web server) but you may need some extra configuration work on the web server side if you are using a different web server. In particular you need to tell your web server where the certificates are located on local host and that it needs to verify certificates coming from the clients. How to do it is web server dependent and therefore omitted here.

Multiple login forms

Some login methods modify the login_form, some do not. When they do that, they may not be able to coexist. Yet some coexist by providing multiple login forms in the same page. web2py provides a way to do it. Here is an example mixing normal login (auth) and RPX login (janrain.com):

  1. from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm
  2. other_form = RPXAccount(request, api_key='...', domain='...', url='...')
  3. auth.settings.login_form = ExtendedLoginForm(auth, other_form, signals=['token'])

If signals are set and a parameter in request matches any signals, it will return the call of other_form.login_form instead. other_form can handle some particular situations, for example, multiple steps of OpenID login inside other_form.login_form.

Otherwise it will render the normal login form together with the other_form.

Record versioning

You can use Auth to enable full record versioning:

  1. auth.enable_record_versioning(db,
  2. archive_db=None,
  3. archive_names='%(tablename)s_archive',
  4. current_record='current_record'):

This tells web2py to create an archive table for each of the tables in db and store a copy of each record when modified. The old copy is stored. The new copy is not.

The last three parameters are optional:

  • archive_db allows to specify another database where the archive tables are to be stored. Setting it to None is the same as setting it to db.
  • archive_names provides a pattern for naming each archive table.
  • current_record specified the name of the reference field to be used in the archive table to refer to the original, unmodified, record. Notice that archive_db!=db then the reference field is just an integer field since cross database references are not possible.

Only tables with modified_by and modified_on fields (as created for example by auth.signature) will be archived.

When you enable_record_versioning, if records have an is_active field (also created by auth.signature), records will never be deleted but they will be marked with is_active=False. In fact, enable_record_versioning adds a common_filter to every versioned table that filters out records with is_active=False so they essentially become invisible.

If you enable_record_versioning, you should not use auth.archive or crud.archive else you will end up with duplicate records. Those functions do explicitly what enable_record_versioning does automatically and they will be deprecated.

Mail and Auth

You can read more about web2py API for emails and email configuration in Chapter 8. Here we limit the discussion to the interaction between Mail and Auth.

Define a mailer with

  1. from gluon.tools import Mail
  2. mail = Mail()
  3. mail.settings.server = 'smtp.example.com:25'
  4. mail.settings.sender = 'you@example.com'
  5. mail.settings.login = 'username:password'

or simply use the mailer provided by auth:

  1. mail = auth.settings.mailer
  2. mail.settings.server = 'smtp.example.com:25'
  3. mail.settings.sender = 'you@example.com'
  4. mail.settings.login = 'username:password'

You need to replace the mail.settings with the proper parameters for your SMTP server. Set mail.settings.login = None if the SMTP server does not require authentication. If you don’t want to use TLS, set mail.settings.tls = False

In Auth, by default, email verification is disabled. To enable email, append the following lines in the model where auth is defined:

  1. auth.settings.registration_requires_verification = True
  2. auth.settings.registration_requires_approval = False
  3. auth.settings.reset_password_requires_verification = True
  4. auth.messages.verify_email = 'Click on the link %(link)s to verify your email'
  5. auth.messages.reset_password = 'Click on the link %(link)s to reset your password'

In the two auth.messages above, you may need to replace the URL portion of the string with the proper complete URL of the action. This is necessary because web2py may be installed behind a proxy, and it cannot determine its own public URLs with absolute certainty. The above examples (which are the default values) should, however, work in most cases.

Two-step verification

Two-step verification (or Two-factor authentication) is a way of improving authentication security. The setting adds an extra step in the login process. In the first step, users are shown the standard username/password form. If they successfully pass this challenge by submitting the correct username and password, and two-factor authentication is enabled for the user, the server will present a second form before logging them in.

image

This functionality can be enabled on a per-user basis:

This case is a good example for apps where users can enable/disable two-factor authentication by them self.

This form will ask users for a six-digit code that has been emailed to their accounts (the server emails the code if the username and password was correct). By default the user will 3 attempts to introduce the code. If the code is incorrect after 3 attempts, the second verification step is treated as having failed and the user must complete the first challenge (username/password) again.

  • Create a group (also known as a role) for the two-step verification. In this example it will be called auth2step and the description may be Two-step verification.
  • Give a user membership of this role.
  • Add the following setting in the model where you created and configured your auth object (probably in the model db.py):
  1. auth.settings.two_factor_authentication_group = "auth2step"
  • Don’t forget to configure the email server in db.py
This functionality can be enabled for the entire app:

This form will ask users for a six-digit code that has been emailed to their accounts (the server emails the code if the username and password was correct). By default the user will 3 attempts to introduce the code. If the code is incorrect after 3 attempts, the second verification step is treated as having failed and the user must complete the first challenge (username/password) again.

  1. auth.settings.auth_two_factor_enabled = True

This case will effect over all the user in the application. For example, if your office IP is 93.56.854.54 and you don’t want two-factor authentication from your office IP. In your models:

  1. if request.env.remote_addr != '93.56.854.54':
  2. auth.settings.auth_two_factor_enabled = True
Other options that can be applied over the examples before:
Example 1: If you want to send the code by SMS instead of email. In your models write:
  1. def _sendsms(user, auth_two_factor):
  2. #write the process to send the auth_two_factor code by SMS
  3. return auth_two_factor
  4. auth.settings.auth_two_factor_enabled = True
  5. auth.messages.two_factor_comment = "Your code have been sent by SMS"
  6. auth.settings.two_factor_methods = [lambda user, auth_two_factor: _sendsms(user, auth_two_factor)]

For def _sendsms(...) receive two values: user and auth_two_factor:

  • user: it is a row with all his parameters. You can access them: user.email, user.first_name, etc.
  • auth_two_factor: string that contains the authentication code.

Note that in case you want to send an SMS, you will need to add extra field, for example phone in your user table. In this case you can access to the phone field as user.phone. More info how to send an SMS with web2py Emails-and-SMS

Example 2: If you want to send the code by SMS and create or own code:
  1. def _sendsms(user, auth_two_factor):
  2. auth_two_factor = #write your own algorithm to generate the code.
  3. #write the process to send the auth_two_factor code by SMS
  4. return auth_two_factor
  5. auth.settings.two_factor_methods = [lambda user, auth_two_factor: _sendsms(user, auth_two_factor)]
Example 3: The code is generated by a external client. For example Mobile OTP Client:

MOTP (Mobile one time password) allows you to login with a one time password (OTP) generated on a motp client, motp clients are available for practically all platforms. To know more about OTP visit wiki-One-time-password to know more visit MOTP

For the next example we will use DroidOTP. It is a free app and it can be found in Play Store for Android. Once you have installed:

  • Create a new profile, for example test
  • Initialize a secret key shaking your phone.

In your models copy and paste:

  1. #Before define tables, we add some extra field to auth_user
  2. auth.settings.extra_fields['auth_user'] = [
  3. Field('motp_secret', 'password', length=512, default='', label='MOTP Secret'),
  4. Field('motp_pin', 'string', length=128, default='', label='MOTP PIN')]
  5. OFFSET = 60 #Be sure is the same in your OTP Client
  6. #Set session.auth_two_factor to None. Because the code is generated by external app.
  7. # This will avoid to use the default setting and send a code by email.
  8. def _set_two_factor(user, auth_two_factor):
  9. return None
  10. def verify_otp(user, otp):
  11. import time
  12. from hashlib import md5
  13. epoch_time = int(time.time())
  14. time_start = int(str(epoch_time - OFFSET)[:-1])
  15. time_end = int(str(epoch_time + OFFSET)[:-1])
  16. for t in range(time_start - 1, time_end + 1):
  17. to_hash = str(t) + user.motp_secret + user.motp_pin
  18. hash = md5(to_hash).hexdigest()[:6]
  19. if otp == hash:
  20. return hash
  21. auth.settings.auth_two_factor_enabled = True
  22. auth.messages.two_factor_comment = "Verify your OTP Client for the code."
  23. auth.settings.two_factor_methods = [lambda user, auth_two_factor: _set_two_factor(user, auth_two_factor)]
  24. auth.settings.two_factor_onvalidation = [lambda user, otp: verify_otp(user, otp)]

The secret key generated before with your phone need to be introduced into motp_secret field. The secret should be not reused, for security reasons. Choose one PIN. It can be numbers, letters or a mix. Go to your phone, choose your profile and type the PIN you have introduced before in the form. You got the authenticator code to use in your app!!

image

Note that for this way of two-factor authentication phone and server (where web2py app is hosted) need to be synchronized (on time). They can be in a different time zone. This is because the OTP use Unix time stamp. It tracks the time as a running total of seconds.

Some extra parameters for configuration:

Set your custom attempts to login:

  1. auth.setting.auth_two_factor_tries_left = 3

Message to return in case the code is incorrect:

  1. auth.messages.invalid_two_factor_code = 'Incorrect code. {0} more attempt(s) remaining.'

To customize the email template:

  1. auth.messages.retrieve_two_factor_code='Your temporary login code is {0}'
  2. auth.messages.retrieve_two_factor_code_subject='Your temporary login code is {0}'

To customize two-factor form:

  1. auth.messages.label_two_factor = 'Authentication code'
  2. auth.messages.two_factor_comment = 'The code was emailed to you and is required for login.'