Central Authentication Service

web2py provides support for third party authentication and single sign on. Here we discuss the Central Authentication Service (CAS) which is an industry standard and both client and server are built-into web2py.

CAS is an open protocol for distributed authentication and it works in the following way: When a visitor arrives at our web site, our application check in the session if the user is already authenticated (for example via a session.token object). If the user is not authenticated, the controller redirects the visitor from the CAS appliance, where the user can log in, register, and manage his credentials (name, email and password). If the user registers, he receives an email, and registration is not complete until he responds to the email. Once the user has successfully registered and logged in, the CAS appliance redirects the user to our application together with a key. Our application uses the key to get the credentials of the user via an HTTP request in the background to the CAS server.

Using this mechanism, multiple applications can use a single sign-on via a single CAS server. The server providing authentication is called a service provider. Applications seeking to authenticate visitors are called service consumers.

CAS is similar to OpenID, with one main difference. In the case of OpenID, the visitor chooses the service provider. In the case of CAS, our application makes this choice, making CAS more secure.

Running a web2py CAS provider is as easy as copying the scaffolding app. In fact any web2py app that exposes the action

  1. ## in provider app
  2. def user(): return dict(form=auth())

is a CAS 2.0 provider and its services can be accessed at the URL

  1. http://.../provider/default/user/cas/login
  2. http://.../provider/default/user/cas/validate
  3. http://.../provider/default/user/cas/logout

(we assume the app to be called “provider”).

You can access this service from any other web application (the consumer) by simply delegating authentication to the provider:

  1. ## in consumer app
  2. auth = Auth(db, cas_provider='http://127.0.0.1:8000/provider/default/user/cas')

When you visit the login url the consumer app, it will redirect you to the provider app which will perform authentication and will redirect back to the consumer. All processes of registration, logout, change password, retrieve password, have to be completed on the provider app. An entry about the logged-in user will be created on the consumer side so that you add extra fields and have a local profile. Thanks to CAS 2.0 all fields that are readable on the provider and have a corresponding field in the auth_user table of the consumer will be copied automatically.

Auth(..., cas_provider='...') works with third party providers and supports CAS 1.0 and 2.0. The version is detected automatically. By default it builds the URLs of the provider from a base (the cas_provider url above) by appending

  1. /login
  2. /validate
  3. /logout

These can be changed in consumer and in provider

  1. ## in consumer or provider app (must match)
  2. auth.settings.cas_actions['login']='login'
  3. auth.settings.cas_actions['validate']='validate'
  4. auth.settings.cas_actions['logout']='logout'

If you want to connect to a web2py CAS provider from a different domain, you must enable them by appending to the list of allowed domains:

  1. ## in provider app
  2. auth.settings.cas_domains.append('example.com')

Using web2py to authorize non-web2py apps

This is possible but dependent on the web server. here we assume two applications running under the same web server: Apache with mod_wsgi. One of the applications is web2py with an app proving access control via Auth. The other can be a CGI script, a PHP program or anything else. We want to instruct the web server to ask permission to the former application when a client requests access to the latter.

First of all we need to modify the web2py application and add the following controller:

  1. def check_access():
  2. return 'true' if auth.is_logged_in() else 'false'

which returns true if the user is logged in and false otherwise. Now run a web2py process in background:

  1. nohup python web2py.py -a '' -p 8002

Port 8002 is a must and there is no need to enable admin so no admin password.

Then we need to edit the Apache config file (for example “/etc/apache2/sites-available/default”) and instruct apache so that when the non-web2py program is called, it should call the above check action instead and only if it returns true it should proceed and respond to the request, else if should deny access.

Because web2py and the non-web2py application run under the same domain, if the user is logged into the web2py app, the web2py session cookie will be passed to Apache even when the other app is requested and will allow credential verification.

In order to achieve this we need a script, “web2py/scripts/access.wsgi” that can play this trick. The script ships with web2py. All we need to do it tell apache to call this script, the URL of the application needing access control, and the location of the script:

  1. <VirtualHost *:80>
  2. WSGIDaemonProcess web2py user=www-data group=www-data
  3. WSGIProcessGroup web2py
  4. WSGIScriptAlias / /home/www-data/web2py/wsgihandler.py
  5. AliasMatch ^myapp/path/needing/authentication/myfile /path/to/myfile
  6. <Directory /path/to/>
  7. WSGIAccessScript /path/to/web2py/scripts/access.wsgi
  8. </Directory>
  9. </VirtualHost>

Here “^myapp/path/needing/authentication/myfile” is the regular expression that should match the incoming request and “/path/to/“ is the absolute location of the web2py folder.

The “access.wsgi” script contains the following line:

  1. URL_CHECK_ACCESS = 'http://127.0.0.1:8002/%(app)s/default/check_access'

which points to the web2py application we have requested but you can edit it to point to a specific application, running on a port other than 8002.

You can also change the check_access() action and make its logic more complex. This action can retrieve the URL that was originally requested using the environment variable

  1. request.env.request_uri

and you can implement more complex rules:

  1. def check_access():
  2. if not auth.is_logged_in():
  3. return 'false'
  4. elif not user_has_access(request.env.request_uri):
  5. return 'false'
  6. else:
  7. return 'true'