eZ Components - Authentication
Table of Contents
Introduction
Description
The purpose of the Authentication component is to provide support for different means of identification and authentication of users using different providers and protocols.
Class overview
An overview of the most important classes in the Authentication component.
Base classes
- ezcAuthentication
- Main class of Authentication. It is a container for authentication filters, which will be run in sequence. The method run() returns true or false depending on the success of the authentication filters.
- ezcAuthenticationCredentials
- Structure which holds user credentials. Types are id credentials (ezcAuthenticationIdCredentials) and id + password credentials (ezcAuthenticationPasswordCredentials). Usually there is only one credentials object in the application. Multiple credentials can be used via the ezcAuthenticationGroupFilter class.
- ezcAuthenticationSession
- Used to store the authenticated username and the timestamp between requests.
Authentication filters
- ezcAuthenticationDatabaseFilter
- Filter to authenticate against a database. Uses a database instance provided by the Database component (via the ezcDbInstance::get() function). It depends on the Database component, so it is implemented in the the tie-in component AuthenticationDatabaseTiein.
- ezcAuthenticationGroupFilter
- Container filter for 2 or more filters. Depending on configuration, at least one filter needs to succeed in order for the group to succeed, or all filters need to succeed in order for the group to succeed.
- ezcAuthenticationHtpasswdFilter
- Filter to authenticate against a htpasswd password file. Supports the same encryption methods as the Unix command htpasswd, and the encryption method is detected automatically from the file.
- ezcAuthenticationLdapFilter
- Filter to authenticate against an LDAP directory. For now the password can be only in plain text. It depends on the PHP ldap extension.
- ezcAuthenticationOpenidFilter
- Filter to authenticate against OpenID. For now the OpenID versions 1.0 and 1.1 are supported.
- ezcAuthenticationTokenFilter
- Filter used to implement CAPTCHA tests. It basically compares the server generated token with the value entered by the user, using a specified hashing callback function.
Stores
OpenID uses a store to hold the generated nonces and the associations (in "smart" mode). If there is no store specified, then nonces are not checked.
- ezcAuthenticationOpenidStore
- Abstract class from which the different stores inherit.
- ezcAuthenticationOpenidFileStore
- Uses file storage. Nonces are stored in files named after the nonce itself, and associations are stored in files named after the OpenID provider with which the association is made.
- ezcAuthenticationOpenidDbStore
- Database storage. Nonces and associations are stored in two tables, with names defined as options in ezcAuthenticationOpenidDbStoreOptions. Implemented in AuthenticationDatabaseTiein.
General authentication
Stateless authentication
The general template for authentication is:
get the user credentials (eg. in a ezcAuthenticationPasswordCredentials object)
create an object of class ezcAuthentication and pass the credentials object to it
add authentication filters to the authentication object with addFilter()
call the run() method of the authentication object
call the getStatus() method of the authentication object and analyse it
The following example demonstrates the above steps.
- <?php
- require_once 'tutorial_autoload.php';
- $credentials = new ezcAuthenticationPasswordCredentials( 'jan.modaal', 'b1b3773a05c0ed0176787a4f1574ff0075f7521e' );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->addFilter( new ezcAuthenticationHtpasswdFilter( '/etc/htpasswd' ) );
- // add more filters if needed
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- 'ezcAuthenticationHtpasswdFilter' => array(
- ezcAuthenticationHtpasswdFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username',
- ezcAuthenticationHtpasswdFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password'
- )
- );
- foreach ( $status as $line )
- {
- list( $key, $value ) = each( $line );
- echo $err[$key][$value] . "\n";
- }
- }
- else
- {
- // authentication succeeded, so allow the user to see his content
- }
- ?>
First, a credentials object is created with username jan.modaal and password 'b1b3773a05c0ed0176787a4f1574ff0075f7521e' (sha1() hash).
An authentication object is created using the credentials object, and a htpasswd filter (using the /etc/htpasswd file) is added to it.
After running the authentication (line 8), if the username and the password do not pass through the htpasswd filter, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Username incorrect", "Password incorrect").
If run() returned true (line 24) then the user is logged-in and he can see his content.
Using session
The following example shows how to use the session class to store authentication information (username and timestamp) between requests.
- <?php
- require_once 'tutorial_autoload.php';
- // no headers should be sent before calling $session->start()
- $session = new ezcAuthenticationSession();
- $session->start();
- $user = isset( $_POST['user'] ) ? $_POST['user'] : $session->load();
- $password = isset( $_POST['password'] ) ? $_POST['password'] : null;
- $credentials = new ezcAuthenticationPasswordCredentials( $user, $password );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = $session;
- $authentication->addFilter( new ezcAuthenticationHtpasswdFilter( '/etc/htpasswd' ) );
- // add other filters if needed
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- 'ezcAuthenticationHtpasswdFilter' => array(
- ezcAuthenticationHtpasswdFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username',
- ezcAuthenticationHtpasswdFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password'
- ),
- 'ezcAuthenticationSession' => array(
- ezcAuthenticationSession::STATUS_EMPTY => '',
- ezcAuthenticationSession::STATUS_EXPIRED => 'Session expired'
- )
- );
- foreach ( $status as $line )
- {
- list( $key, $value ) = each( $line );
- echo $err[$key][$value] . "\n";
- }
- }
- else
- {
- // authentication succeeded, so allow the user to see his content
- }
- ?>
A session class is created and used to start the PHP session. The username and password (provided by the user through a POST form) are fetched and used to create a credentials object.
An authentication object is created using the credentials object, and the session class is added to the authentication object. In addition an htpasswd filter (using the /etc/htpasswd file) is added to the authentication object.
After running the authentication (line 14), if the username and the password do not pass through the session class or the htpasswd filter, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Username incorrect", "Password incorrect", "Session expired").
If run() returned true (line 34) then the user is logged-in and he can see his content.
Improving authentication performance when using the session
When using the session, it is often desirable to take advantage of the fact that the authenticated state of the user is kept in the session and not create and initialize the other filters (which might slow things down on every request).
The application can be structured like this:
- <?php
- $session = new ezcAuthenticationSession();
- $session->start();
- $credentials = new ezcAuthenticationPasswordCredentials( $user, $pass );
- $authenticated = false;
- if ( !$session->isValid( $credentials ) )
- {
- // create the authentication object
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = $session;
- // create filters and add them to the authentication object
- $authentication->addFilter( new ezcAuthenticationOpenidFilter() );
- // run the authentication object
- if ( !$authentication->run() )
- {
- $status = $authentication->getStatus();
- // build an error message based on $status
- }
- else
- {
- $authenticated = true;
- }
- }
- else
- {
- $authenticated = true;
- }
- if ( $authenticated )
- {
- // the authentication succeeded and the user can see his content
- }
- else
- {
- // inform the user that the authentication failed (with the error
- // message that was created earlier)
- } ?>
In this way, the creation and initialization of the authentication filters is not performed if the credentials are stored in the session.
Authentication filters
Database
See the AuthenticationDatabaseTiein component.
Group
The following example shows how to use a group filter to authenticate against EITHER a database or an LDAP directory.
- <?php
- require_once 'tutorial_autoload.php';
- $credentials = new ezcAuthenticationPasswordCredentials( 'jan.modaal', 'qwerty' );
- // create a database filter
- $database = new ezcAuthenticationDatabaseInfo( ezcDbInstance::get(), 'users', array( 'user', 'password' ) );
- $databaseFilter = new ezcAuthenticationDatabaseFilter( $database );
- // create an LDAP filter
- $ldap = new ezcAuthenticationLdapInfo( 'localhost', 'uid=%id%', 'dc=example,dc=com', 389 );
- $ldapFilter = new ezcAuthenticationLdapFilter( $ldap );
- $authentication = new ezcAuthentication( $credentials );
- // use the database and LDAP filters in paralel (only one needs to succeed in
- // order for the user to be authenticated
- $authentication->addFilter( new ezcAuthenticationGroupFilter( array( $databaseFilter, $ldapFilter ) ) );
- // add more filters if needed
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- 'ezcAuthenticationLdapFilter' => array(
- ezcAuthenticationLdapFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username',
- ezcAuthenticationLdapFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password'
- ),
- 'ezcAuthenticationDatabaseFilter' => array(
- ezcAuthenticationDatabaseFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username',
- ezcAuthenticationDatabaseFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password'
- )
- );
- foreach ( $status as $line )
- {
- list( $key, $value ) = each( $line );
- echo $err[$key][$value] . "\n";
- }
- }
- else
- {
- // authentication succeeded, so allow the user to see his content
- }
- ?>
First, a credentials object is created with username 'jan.modaal' and password 'qwerty'.
An authentication object is created using the credentials object. A group filter is added to it, consisting of a Database filter and an LDAP filter.
After running the authentication (line 19), if the username and the password do not pass through any of the filters in the group, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Username incorrect", "Password incorrect").
If run() returned true (line 39) then the user is logged-in and he can see his content.
Multiple credentials
To be able to use multiple credentials for authentication (each filter with its own credentials), you must enable the multipleCredentials option for ezcAuthenticationGroupFilter.
The following example demonstrates how to use multiple credentials.
- <?php
- require_once 'tutorial_autoload.php';
- $credentials1 = new ezcAuthenticationPasswordCredentials( 'jan.modaal', 'b1b3773a05c0ed0176787a4f1574ff0075f7521e' ); // incorrect password
- $credentials2 = new ezcAuthenticationPasswordCredentials( 'john.doe', 'wpeE20wyWHnLE' ); // correct username + password
- $options = new ezcAuthenticationGroupOptions();
- $options->multipleCredentials = true;
- $options->mode = ezcAuthenticationGroupFilter::MODE_AND;
- $group = new ezcAuthenticationGroupFilter( array(), $options );
- $group->addFilter( new ezcAuthenticationHtpasswdFilter( '../../tests/filters/htpasswd/data/htpasswd' ), $credentials1 );
- $group->addFilter( new ezcAuthenticationHtpasswdFilter( '../../tests/filters/htpasswd/data/htpasswd' ), $credentials2 );
- $authentication = new ezcAuthentication( $credentials1 );
- $authentication->addFilter( $group );
- // add more filters if needed
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- array( 'ezcAuthenticationHtpasswdFilter' => array(
- ezcAuthenticationHtpasswdFilter::STATUS_OK => '',
- ezcAuthenticationHtpasswdFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username ' . $credentials1->id,
- ezcAuthenticationHtpasswdFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password for ' . $credentials1->id
- ) ),
- array( 'ezcAuthenticationHtpasswdFilter' => array(
- ezcAuthenticationHtpasswdFilter::STATUS_OK => '',
- ezcAuthenticationHtpasswdFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username ' . $credentials2->id,
- ezcAuthenticationHtpasswdFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password for ' . $credentials2->id
- ) )
- );
- foreach ( $status as $line => $error )
- {
- list( $key, $value ) = each( $error );
- echo $err[$line][$key][$value] . "\n";
- }
- }
- else
- {
- // authentication succeeded, so allow the user to see his content
- }
- ?>
First, two credentials objects are created.
A Group filter is created with the multipleCredentials option enabled.
Two Htpasswd filters are added to the Group filter, each with their own credentials.
An Authentication object is created with a default credentials (which would have been used for other filters outside the Group filter, and to save the authenticated state in the session).
The Group filter is then added to the Authentication object.
After running the authentication (line 19), if the usernames and the passwords do not pass through the htpasswd filters, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Username incorrect john.doe", "Password incorrect for john.doe", etc).
If run() returned true (line 44) then the user is logged-in and he can see his content.
The above example will output:
Incorrect password for jan.modaal
Htpasswd
The following example shows how to authenticate against an htpasswd file.
- <?php
- require_once 'tutorial_autoload.php';
- $credentials = new ezcAuthenticationPasswordCredentials( 'jan.modaal', 'b1b3773a05c0ed0176787a4f1574ff0075f7521e' );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = new ezcAuthenticationSession();
- $authentication->addFilter( new ezcAuthenticationHtpasswdFilter( '/etc/htpasswd' ) );
- // add other filters if needed
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- 'ezcAuthenticationHtpasswdFilter' => array(
- ezcAuthenticationHtpasswdFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username',
- ezcAuthenticationHtpasswdFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password'
- )
- );
- foreach ( $status as $line )
- {
- list( $key, $value ) = each( $line );
- echo $err[$key][$value] . "\n";
- }
- }
- else
- {
- // authentication succeeded, so allow the user to see his content
- }
- ?>
First, a credentials object is created with username jan.modaal and password 'b1b3773a05c0ed0176787a4f1574ff0075f7521e' (sha1() hash).
An authentication object is created using the credentials object, and a htpasswd filter (using the /etc/htpasswd file) is added to it.
After running the authentication (line 9), if the username and the password do not pass through the htpasswd filter, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Username incorrect", "Password incorrect").
If run() returned true (line 25) then the user is logged-in and he can see his content.
LDAP
The following example shows how to authenticate agains an LDAP directory.
- <?php
- require_once 'tutorial_autoload.php';
- $credentials = new ezcAuthenticationPasswordCredentials( 'jan.modaal', 'qwerty' );
- $ldap = new ezcAuthenticationLdapInfo( 'localhost', 'uid=%id%', 'dc=example,dc=com', 389 );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->addFilter( new ezcAuthenticationLdapFilter( $ldap ) );
- // add more filters if needed
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- 'ezcAuthenticationLdapFilter' => array(
- ezcAuthenticationLdapFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username',
- ezcAuthenticationLdapFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password'
- )
- );
- foreach ( $status as $line )
- {
- list( $key, $value ) = each( $line );
- echo $err[$key][$value] . "\n";
- }
- }
- else
- {
- // authentication succeeded, so allow the user to see his content
- }
- ?>
First, a credentials object is created with username jan.modaal and password 'qwerty'.
An authentication object is created using the credentials object, and an LDAP filter is added to it. The $ldap structure specifies the LDAP host (localhost), the format of the directory entry (%id% is a placeholder which will be replaced by the actual value at bind time), the base of the directory entry ('dc=example,dc=com') and the port on which to connect to the host (389).
After running the authentication (line 7), if the username and the password do not pass through the LDAP filter, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Username incorrect", "Password incorrect").
If run() returned true (line 34) then the user is logged-in and he can see his content.
Fetch extra data during LDAP authentication
Any data that is defined for an acount can be fetched. Before running the authentication process (before calling run(), register which data needs to be fetched. Example:
// $filter is an ezcAuthenticationLdapFilter object
$filter->registerFetchData( array( 'name', 'company', 'mobile' ) );
After the authentication process is finished (after run()), retrieve the data that was registered:
// $filter is an ezcAuthenticationLdapFilter object
$data = $filter->fetchData();
For the previous example, the $data array will be something like this:
array( 'name' => array( 'Dr. No' ),
'company' => array( 'SPECTRE' ),
'mobile' => array( '555-7732873' )
);
OpenID
OpenID has 2 modes of operation: dumb and smart. These modes define the way the consumer (the application server, on which the Components run) is communicating with the OpenID provider (another server where users authenticate with their OpenID username and password; there are many of these and users can register on which one they want).
Dumb mode (stateless)
In this mode there are 3 http requests:
- Discovery
- The consumer requests the URL which the user entered and finds out the URL of the provider.
- openid.checkid_setup
- The consumer redirects the browser to the provider, so that the user can enter his username and password to the provider. The provider then redirects back to the consumer. The URL to which the provider redirects back can be configured with the OpenID option redirectUrl (see ezcAuthenticationOpenidOptions). The checkid_immediate mode is supported as well, for authentication in a pop-up window or iframe (or similar techniques). See below the section OpenID immediate mode for details.
- openid.check_authentication
- The consumer sends to the provider the values received in step 2 and receives the information if the user is authenticated or not.
Smart mode (keeping state)
In this mode there are also 3 http requests, but only 2 every time and 1 request from time to time:
- Discovery
- Same as in dumb mode.
- openid.checkid_setup
- Same as in dumb mode, but the handle associated with the shared secret is sent as well.
The extra request (which is done from time to time) is:
- openid.associate
- The consumer and the provider establish a shared secret, which the consumer uses when it redirects in step 2, and it will use the same secret for all requests to the same provider. Step 3 (openid.check_authentication) is not required anymore. The shared secret has a timeout period, so it must be renewed from time to time.
OpenID "dumb" (stateless) mode
The following example shows how to authenticate against OpenID in "dumb" (stateless) mode.
- <?php
- require_once 'tutorial_autoload.php';
- // no headers should be sent before calling $session->start()
- $session = new ezcAuthenticationSession();
- $session->start();
- $url = isset( $_GET['openid_identifier'] ) ? $_GET['openid_identifier'] : $session->load();
- $action = isset( $_GET['action'] ) ? strtolower( $_GET['action'] ) : null;
- $credentials = new ezcAuthenticationIdCredentials( $url );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = $session;
- if ( $action === 'logout' )
- {
- $session->destroy();
- }
- else
- {
- $filter = new ezcAuthenticationOpenidFilter();
- $authentication->addFilter( $filter );
- }
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- 'ezcAuthenticationOpenidFilter' => array(
- ezcAuthenticationOpenidFilter::STATUS_SIGNATURE_INCORRECT => 'OpenID said the provided identifier was incorrect',
- ezcAuthenticationOpenidFilter::STATUS_CANCELLED => 'The OpenID authentication was cancelled',
- ezcAuthenticationOpenidFilter::STATUS_URL_INCORRECT => 'The identifier you provided is invalid'
- ),
- 'ezcAuthenticationSession' => array(
- ezcAuthenticationSession::STATUS_EMPTY => '',
- ezcAuthenticationSession::STATUS_EXPIRED => 'Session expired'
- )
- );
- foreach ( $status as $line )
- {
- list( $key, $value ) = each( $line );
- echo $err[$key][$value] . "\n";
- }
- ?>
- Please login with your OpenID identifier (an URL, eg. www.example.com or http://www.example.com):
- <form method="GET" action="">
- <input type="hidden" name="action" value="login" />
- <img src="http://openid.net/login-bg.gif" /> <input type="text" name="openid_identifier" />
- <input type="submit" value="Login" />
- </form>
- <?php
- }
- else
- {
- ?>
- You are logged-in as <b><?php echo $url; ?></b> | <a href="?action=logout">Logout</a>
- <?php
- }
- ?>
A session class is created and used to start the PHP session. The OpenID identifier (provided by the user through a GET form) is fetched and used to create a credentials object. On subsequent requests to the page, the token is loaded from session instead of the GET form. OpenID specifications recommend the name 'openid_identifier' for the text field of the form in which users type their OpenID identifier (so that browser can prefill the field if user chooses this).
An authentication object is created using the credentials object, and the session handler is added to it.
If the user is at logout (line 15), then the session is destroyed, which means the user will see the login form.
If the user is not at logout (line 19), then an OpenID filter is created with the credentials object.
After running the authentication (line 25), if the OpenID server did not authorize the identifier, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Signature incorrect", "Session expired"). At line 46 a simple HTML form is displayed, as example. The form displays the OpenID logo (as suggested by the OpenID specifications).
If run() returned true (line 55) then the user is logged-in and he can see his content. Line 59 contains an example of how to implement a logout option for the application.
OpenID "smart" (stateful) mode
The following example shows how to authenticate against OpenID in "smart" (stateful) mode.
- <?php
- require_once 'tutorial_autoload.php';
- // no headers should be sent before calling $session->start()
- $options = new ezcAuthenticationSessionOptions();
- $session = new ezcAuthenticationSession( $options );
- $session->start();
- // URL after returning from OpenID authentication
- $url = isset( $_GET['openid_identity'] ) ? $_GET['openid_identity'] : $session->load();
- if ( $url === null )
- {
- // URL at the start of authentication
- $url = isset( $_GET['openid_identifier'] ) ? $_GET['openid_identifier'] : $session->load();
- }
- $action = isset( $_GET['action'] ) ? strtolower( $_GET['action'] ) : null;
- $credentials = new ezcAuthenticationIdCredentials( $url );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = $session;
- if ( $action === 'logout' )
- {
- $session->destroy();
- }
- else
- {
- $options = new ezcAuthenticationOpenidOptions();
- $options->mode = ezcAuthenticationOpenidFilter::MODE_SMART;
- $options->openidVersion = ezcAuthenticationOpenidFilter::VERSION_2_0;
- $options->store = new ezcAuthenticationOpenidFileStore( '/tmp/store' );
- $filter = new ezcAuthenticationOpenidFilter( $options );
- $filter->registerFetchData( array( 'fullname', 'gender', 'country', 'language' ) );
- $authentication->addFilter( $filter );
- }
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array();
- $err["user"] = "";
- $err["session"] = "";
- for ( $i = 0; $i < count( $status ); $i++ )
- {
- list( $key, $value ) = each( $status[$i] );
- switch ( $key )
- {
- case 'ezcAuthenticationOpenidFilter':
- if ( $value === ezcAuthenticationOpenidFilter::STATUS_SIGNATURE_INCORRECT )
- {
- $err["user"] = "<span class='error'>OpenID said the provided identifier was incorrect.</span>";
- }
- if ( $value === ezcAuthenticationOpenidFilter::STATUS_CANCELLED )
- {
- $err["user"] = "<span class='error'>The OpenID authentication was cancelled, please re-login.</span>";
- }
- if ( $value === ezcAuthenticationOpenidFilter::STATUS_URL_INCORRECT )
- {
- $err["user"] = "<span class='error'>The identifier you provided is empty or invalid. It must be a URL (eg. www.example.com or http://www.example.com)</span>";
- }
- break;
- case 'ezcAuthenticationSessionFilter':
- if ( $value === ezcAuthenticationSessionFilter::STATUS_EXPIRED )
- {
- $err["session"] = "<span class='error'>Session expired</span>";
- }
- break;
- }
- }
- ?>
- <style>
- .error {
- color: #FF0000;
- }
- </style>
- Please login with your OpenID identifier (an URL, eg. www.example.com or http://www.example.com):
- <form method="GET" action="">
- <input type="hidden" name="action" value="login" />
- <img src="http://openid.net/login-bg.gif" /> <input type="text" name="openid_identifier" />
- <input type="submit" value="Login" />
- <?php echo $err["user"]; ?> <?php echo $err["session"]; ?>
- </form>
- <?php
- }
- else
- {
- ?>
- You are logged-in as <b><?php echo $url; ?></b> | <a href="?action=logout">Logout</a>
- <?php
- }
- ?>
The only differences between this example and the one in the previous section is defining the mode of the OpenID filter, and defining a store (file store in this example or database store as shown in the OpenID example in AuthenticationDatabaseTiein) which will hold the associations. In addition the store will also hold the nonces which are used to prevent replay attacks.
The example also introduces the OpenID 2.0 version features, namely the possibility to use a common URL for all users of an OpenID provider (for example using http://yahoo.com in the login box), and then being redirected to the OpenID provider to enter the provider credentials, and in the end being redirected back with the unique user OpenID URL in the response from the OpenID provider (in the $url variable in the example above). See the section OpenID 2.0 below for more information.
OpenID immediate mode
The OpenID request checkid_immediate is supported, which allows for user authentication in a pop-up window or iframe (or similar techniques). Instead of redirecting the user agent as in the checkid_setup step, the developer has the possibility to open a pop-up/iframe for the user to authenticate with the OpenID provider.
A more detailed description of the process: when using checkid_immediate, the OpenID provider is asked if the user can be authenticated on the spot, with no redirection of the user agent. If the user cannot be authenticated, the provider sends back a setup URL, which the application can use in a pop-up window or iframe to display to the user so that he can authenticate himself to the OpenID provider. After user enters his OpenID username and password at this page and accepts the originating site, the pop-up window or iframe is redirected to the return URL value (which should be a different page than the page which opens the pop-up window). The return URL page will then inform the main page of success or failure through JavaScript, and the main page can do the action that it needs to perform based on the outcome in the pop-up page.
The checkid_immediate mode is enabled by setting the option immediate to true.
Note: retrieval of extra data during authentication (fullname, email, etc) is not possible at the moment when using the immediate mode.
For example, this is one simple way of implementing checkid_immediate:
the main page contains the OpenID login form (where the user types in his OpenID identifier). This page contains also a hidded form value which specifies to which page to return to in the pop-up window. The Enter key and the submit button should be disabled on the form. When user clicks on the Login button, the main page should employ AJAX to request the return URL. When the return URL finishes loading, the main page will read from the return URL page the setup URL and it will open it in a pop-up/iframe.
the return URL page enables the option immediate to the OpenID filter, and runs the filter. It gets back the setup URL and it echoes it to be picked-up by the main page once the return URL page will finish loading. The setup URL should be the only thing that the return URL page is echoing, to not interfere with the main page.
in the pop-up/iframe the setup URL will load, which basically depends on the OpenID provider how it is handled by the user. After the user enters his credentials on the setup URL page, he will be redirected to the return URL, which should detect this, and which should inform the main page that the user was authenticated to the OpenID provider.
A rudimentary source code example is provided below. It does not contain code to inform the user that the session expired or the errors experienced during the authentication process. The code has been tested on some browsers (Firefox 1.5, Konqueror 3.5, Internet Explorer 6.0), but it is possible that some browsers might have issues with the JavaScript code.
The main page:
- <?php
- require_once 'tutorial_autoload.php';
- // no headers should be sent before calling $session->start()
- $options = new ezcAuthenticationSessionOptions();
- // setting 60 seconds timeout for session for testing purposes only
- $options->validity = 60;
- $session = new ezcAuthenticationSession( $options );
- $session->start();
- $identity = $session->load();
- $url = isset( $_GET['openid_identifier'] ) ? $_GET['openid_identifier'] : $identity;
- $action = isset( $_GET['action'] ) ? strtolower( $_GET['action'] ) : null;
- $credentials = new ezcAuthenticationIdCredentials( $url );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = $session;
- if ( $action === 'logout' )
- {
- $session->destroy();
- }
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- ?>
- <script language="JavaScript">
- var xmlhttp = false;
- /*@cc_on @*/
- /*@if ( @_jscript_version >= 5 )
- try
- {
- xmlhttp = new ActiveXObject( "Msxml2.XMLHTTP" );
- }
- catch ( e )
- {
- try
- {
- xmlhttp = new ActiveXObject( "Microsoft.XMLHTTP" );
- }
- catch ( E )
- {
- xmlhttp = false;
- }
- }
- @end @*/
- if ( !xmlhttp && typeof XMLHttpRequest != 'undefined' )
- {
- try
- {
- xmlhttp = new XMLHttpRequest();
- }
- catch ( e )
- {
- xmlhttp = false;
- }
- }
- if ( !xmlhttp && window.createRequest )
- {
- try
- {
- xmlhttp = window.createRequest();
- }
- catch ( e )
- {
- xmlhttp = false;
- }
- }
- </script>
- <script language="JavaScript">
- function disableEnterKey( e )
- {
- var key;
- key = ( window.event ) ? window.event.keyCode : e.which;
- return ( key != 13 );
- }
- function login()
- {
- var url;
- var form1;
- var setupUrl;
- form1 = document.form1;
- url = form1.url.value + '?openid_identifier=' + escape( form1.openid_identifier.value ) +
- '&action=login&immediate=true';
- xmlhttp.open( "GET", url, true );
- xmlhttp.onreadystatechange = function()
- {
- if ( xmlhttp.readyState == 4 )
- {
- setupUrl = xmlhttp.responseText;
- hwnd = window.open( setupUrl, 'Login' );
- if ( hwnd.opener == null )
- {
- hwnd.opener = self;
- }
- }
- }
- xmlhttp.send( null );
- }
- </script>
- Please login with your OpenID identifier (an URL, eg. www.example.com or http://www.example.com):
- <form method="GET" name="form1">
- <input type="hidden" name="url" value="http://localhost/openid/openid_immediate.php" />
- <input type="hidden" name="action" value="login" />
- <img src="http://openid.net/login-bg.gif" /> <input type="text" name="openid_identifier" onKeyPress="return disableEnterKey( event )" />
- <input type="button" onclick="javascript: login();" value="Login" />
- <div name="status" id="status"></div>
- </form>
- <?php
- }
- else
- {
- ?>
- You are logged-in as <b><?php echo $url; ?></b> | <a href="?action=logout">Logout</a>
- <?php
- }
- ?>
This page handles the session, and contains JavaScript code to read from the return URL page the setup URL, and to open a pop-up page with that setup URL.
The pop-up page (also return URL):
- <?php
- require_once 'tutorial_autoload.php';
- // no headers should be sent before calling $session->start()
- $options = new ezcAuthenticationSessionOptions();
- // setting 10 seconds timeout for session for testing purposes only
- $options->validity = 60;
- $session = new ezcAuthenticationSession( $options );
- $session->start();
- $setupUrl = isset( $_GET['openid_user_setup_url'] ) ? $_GET['openid_user_setup_url'] : null;
- $immediate = isset( $_GET['immediate'] ) ? $_GET['immediate'] : false;
- if ( $setupUrl !== null )
- {
- $urlParts = parse_url( $setupUrl );
- parse_str( $urlParts['query'], $parts );
- $identity = $parts['openid_identity'];
- }
- else
- {
- $identity = $session->load();
- }
- $url = isset( $_GET['openid_identifier'] ) ? $_GET['openid_identifier'] : $identity;
- $action = isset( $_GET['action'] ) ? strtolower( $_GET['action'] ) : null;
- $credentials = new ezcAuthenticationIdCredentials( $url );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = $session;
- if ( $action === 'logout' )
- {
- $session->destroy();
- }
- else
- {
- $options = new ezcAuthenticationOpenidOptions();
- // for checkid_immediate
- if ( $immediate !== false )
- {
- $options->immediate = true;
- }
- $filter = new ezcAuthenticationOpenidFilter( $options );
- // it seems that fetching extra data does not work with checkid_immediate
- $filter->registerFetchData( array( 'fullname', 'gender', 'country', 'language' ) );
- $authentication->addFilter( $filter );
- }
- if ( !$authentication->run() )
- {
- $setupUrl = $filter->getSetupUrl();
- if ( !empty( $setupUrl ) )
- {
- // the setup URL will be read by the main window
- echo $setupUrl;
- }
- else
- {
- echo 'Authentication did not succeed.';
- }
- }
- else
- {
- ?>
- <script language="JavaScript">
- // inform the parent window that authentication was successful
- top.opener.window.document.getElementById( 'status' ).innerHTML = '<b style="color: #009900;">logged-in</b> | <a href="openid_ajax.php?action=logout">Logout</a>';
- window.close();
- </script>
- <?php
- }
- ?>
This page also contains code to handle the session, in addition to handling the OpenID authentication. It will be called 2 times:
first by the main page, with the immediate=true query parameter. A connection to the OpenID provider is created, and a setup URL is fetched, which will be echoed to be picked up by the main page. The main page then opens the setup URL in a pop-up window.
second, by the setup URL, as the openid.return_to query parameter is set to redirect back to this page. The OpenID filter is called again, this time to check the signature in the return URL, and if it authenticates successfully, the main page is informed of this (line 80) and the pop-up window is closed. Other methods can be employed to inform the main page, and the main page can do other actions instead of just displaying a message.
Fetch extra data during OpenID authentication
Any data that is defined for an acount can be fetched. Before running the authentication process (before calling run(), register which data needs to be fetched. Example:
// $filter is an ezcAuthenticationOpenidFilter object
$filter->registerFetchData( array( 'fullname', 'gender', 'country',
'language' ) );
After the authentication process is finished (after run()), retrieve the data that was registered:
// $filter is an ezcAuthenticationOpenidFilter object
$data = $filter->fetchData();
For the previous example, the $data array will be something like this:
array( 'fullname' => array( 'John Doe' ),
'gender' => array( 'M' ),
'country' => array( 'US' ),
'language' => array( 'FR' )
);
Note: when using the immediate OpenID mode (by setting the option immediate to true), extra data cannot be fetched during authentication.
OpenID 2.0
OpenID 2.0 introduced XRDS discovery of OpenID providers. The user can enter a short URL (same for all users) and by using XRDS discovery the provider can be extracted and the user will be redirected to it, where he will authenticate using his username and password for that provider.
Here is a list with common OpenID providers and the corresponding OpenID provider URLs:
Google https://www.google.com/accounts/o8/id
Yahoo http://yahoo.com/
AOL http://openid.aol.com/{username}
myopenid.com http://{username}.myopenid.com/
flickr http://flickr.com/{username}/
technorati http://technorati.com/people/technorati/{username}/
wordpress http://{username}.wordpress.com
blogspot http://{username}.blogspot.com/
livejournal http://{username}.livejournal.com
claimid http://claimid.com/{username}
myvidoop http://{username}.myvidoop.com/
verisignlabs http://{username}.pip.verisignlabs.com/
So for example a user can login to a website by entering only http://yahoo.com, and the website will redirect the user to this OpenID provider where the user will enter his credentials, and then being redirected to the initial website with the user's unique OpenID URL in the redirect URL. The user's unique OpenID URL will be used by the website to identify the user. See the example in the OpenID "smart" (stateful) mode section.
Technically the login box can be made up of buttons, one for each OpenID provider, and the user would click the one that he/she is using.
For OpenID providers which do not support this feature yet, the only possibility is to use the user's unique OpenID URL from the start.
Token
The following example shows how to create a CAPTCHA test. The example is divided into 2 parts: the initial request (where the user sees the CAPTCHA image and enters the characters he sees in a form) and the follow-up request (after the user submits the form).
On the initial request:
- <?php
- require_once 'tutorial_autoload.php';
- // generate a token and save it in the session or in a file/database
- $pattern = "1234567890abcdefghijklmnopqrstuvwxyz";
- $token = "";
- for( $i = 1; $i <= 6 ; $i++ )
- {
- $token .= $pattern{rand( 0, 36 )};
- }
- $encryptedToken = sha1( $token );
- // save the $encryptedToken in the session
- session_start();
- $_SESSION['encryptedToken'] = $encryptedToken;
- // also generate a distorted image which contains the symbols from $token and use it
- ?>
A 6 characters random token is created and encrypted using sha1(). The token is saved in the session to be available in the follow-up request. An image must be generated from the unencrypted token to be displayed to the user.
On the follow-up request:
- <?php
- require_once 'tutorial_autoload.php';
- // load the $encryptedToken as it was generated on a previous request
- session_start();
- $encryptedToken = isset( $_SESSION['encryptedToken'] ) ? $_SESSION['encryptedToken'] : null;
- // also load the value entered by the user in response to the CAPTCHA image
- $captcha = isset( $_POST['captcha'] ) ? $_POST['captcha'] : null;
- $credentials = new ezcAuthenticationIdCredentials( $captcha );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->addFilter( new ezcAuthenticationTokenFilter( $encryptedToken, 'sha1' ) );
- if ( !$authentication->run() )
- {
- // CAPTCHA was incorrect, so inform the user to try again, eventually
- // by generating another token and CAPTCHA image
- }
- else
- {
- // CAPTCHA was correct, so let the user send his spam or whatever
- }
- ?>
The token generated on the initial request is fetched from the session, and the CAPTCHA value entered by the user is fetched from the POST request.
A credentials object is created from the CAPTCHA value, and it is used to create an authentication object.
A Token filter is created from the stored encrypted token value, and it is added to the authentication object. The second argument of the Token constructor indicates that the CAPTCHA value will be hashed with sha1() before comparing it with the token value.
After calling run() on the authentication object (line 14), if the token and CAPTCHA values don't match, then the CAPTCHA test was incorrect. The developer decides how to handle this situation (user tries again, user is banned, etc).
If the values match (line 19) then it means the user passed the CAPTCHA test (or the bots managed to OCR the image).
TypeKey
The following example shows how to authenticate against TypeKey.
- <?php
- require_once 'tutorial_autoload.php';
- // no headers should be sent before calling $session->start()
- $session = new ezcAuthenticationSession();
- $session->start();
- // $token is used as a key in the session to store the authenticated state between requests
- $token = isset( $_GET['token'] ) ? $_GET['token'] : $session->load();
- $credentials = new ezcAuthenticationIdCredentials( $token );
- $authentication = new ezcAuthentication( $credentials );
- $authentication->session = $session;
- $filter = new ezcAuthenticationTypekeyFilter();
- $authentication->addFilter( $filter );
- // add other filters if needed
- if ( !$authentication->run() )
- {
- // authentication did not succeed, so inform the user
- $status = $authentication->getStatus();
- $err = array(
- 'ezcAuthenticationTypekeyFilter' => array(
- ezcAuthenticationTypekeyFilter::STATUS_SIGNATURE_INCORRECT => 'Signature returned by TypeKey is incorrect',
- ezcAuthenticationTypekeyFilter::STATUS_SIGNATURE_EXPIRED => 'The signature returned by TypeKey expired'
- ),
- 'ezcAuthenticationSession' => array(
- ezcAuthenticationSession::STATUS_EMPTY => '',
- ezcAuthenticationSession::STATUS_EXPIRED => 'Session expired'
- )
- );
- foreach ( $status as $line )
- {
- list( $key, $value ) = each( $line );
- echo $err[$key][$value] . "\n";
- }
- ?>
- <form method="GET" action="https://www.typekey.com/t/typekey/login" onsubmit="document.getElementById('_return').value += '?token=' + document.getElementById('t').value;">
- TypeKey token: <input type="text" name="t" id="t" />
- <input type="hidden" name="_return" id="_return" value="http://localhost/typekey.php" />
- <input type="submit" />
- </form>
- <?php
- }
- else
- {
- // authentication succeeded, so allow the user to see his content
- echo "<b>Logged-in</b>";
- }
- ?>
A session class is created and used to start the PHP session. The TypeKey token (provided by the user through a GET form) is fetched and used to create a credentials object. On subsequent requests to the page, the token is loaded from session instead of the GET form.
An authentication object is created using the credentials object, and a TypeKey filter is added to it, along with the session class.
After running the authentication (line 19), if the TypeKey server did not authorize the token, then the credentials are incorrect and the user must be informed. The getStatus() method is used for this. The values in the status returned must be cycled through and for each value a response is created for the user ("Signature incorrect", "Session expired"). At line 39 an HTML form is displayed, as example on how to attach the TypeKey token to the _return hidded field which will be sent to the TypeKey server.
If run() returned true (line 46) then the user is logged-in and he can see his content.
Fetch extra data during TypeKey authentication
The extra data that can be fetched is name (the TypeKey username), nick (the TypeKey display name) and email (the user email address). The email address can only be fetched if need_email was included in the initial request to the TypeKey server with a value other than 0, and if the user allowed the sharing of his email address. Example:
https://www.typekey.com/t/typekey/login?t=<token>&_return=<url>&need_email=1
After the authentication process is finished (after run()), retrieve the extra data:
// $filter is an ezcAuthenticationTypekeyFilter object
$data = $filter->fetchData();
The $data array will be something like this:
array( 'name' => array( 'john' ),
'nick' => array( 'John Doe' ),
'email' => array( 'john.doe@example.com' ) // or not set
);
Securing applications
Securing applications - A guide to improve the security of online applications. It is not exhaustive, but it provides solutions against common attacks.