Added by Gavin, last edited by Gavin on Apr 10, 2007  (view change)

Labels

 
(None)

Identity and Authentication

Using authentication and sessions for maintaining trust relationships with previously authenticated users in the ZF requires a basic understanding of key concepts and especially a common understanding of key terminology. In this section, our demo application receives additions solving issues of establishing the user's identity and managing semi-persistent information about that user. Significant complexity exists solely from the very nature of authenticating user identities between web browsers and web servers and then managing a trust relationship during the user's session on a website. After studying this tutorial, see the ZF manual for more information about security issues developers need to address in their ZF web-based applications.

Key Concepts: Forms-based Authentication

The vast majority of all web applications use forms-based authentication and rely on [sessions]. The typical web application perform authentication once, in response to some login request, and store a value in the session indicating that the user has already been authenticated. On subsequent requests to the application, the user passing the same cookie value is essentially automatically trusted as having previously authenticated. This prevents having to transmit the user's credentials repeatly, and avoids the potentially expensive process of validating those credentials, or increased exposure to stolen or hijacked credentials.

Security Considerations

After the initial authentication, the session cookie value effectively represents a trusted user, so the [cookie value should be protected].

Key Parts

  1. The user's credentials: This is the data entered by the user and checked against some server-side data store. Most web applications treat the identity and the credentials (i.e., the username and the password) together as an atomic unit during authentication. The credentials could be represented as a list of fields, which will both be extracted from the request, and checked against a secure, server-side data store.
  2. Server-side data store: There are countless ways to keep authentication information on the server. Often, it isn't even on the same server.
  3. The token: Traditionally, a token consists of a flag stored in the user's server-side session indicating the user has been authenticated. Thus, it's traditionally been the session that ties together the application's representation of a user's identity and the authentication subsystem's knowledge of whether or not that identity is authentic and valid. This is really convenient, because the session subsystem does the work of correlating some piece of request data to the "isAuthenticated" flag, the authentication system hasn't had to worry about it.

Authentication Related Workflow

Interesting state transitions include:

  1. anonymous user (no session)
  2. anonymous session - i.e. Zend_Session::start()
  3. requests/page view(s) based on access control settings for unathenticated requests
  4. login page/form view
  5. credential submission (includes some concept of authentication identity)
  6. credential authentication involves extraction of the authentication identity, use of server-side information and algorithms to validate and verify the authentication identity and then map to an authorization identity (assume success for now)
  7. "escalated" privileges allow access to "special" resources by this authorization identity
  8. for successive page views, "deescalated" privileges (e.g. user can't change their password)
  9. eventually either a timeout or "hop count" based "logout" resulting in even less privileges
  10. anonymous (session cookie lost/expired)

Initial authentication (in response to login form submission):

  1. Define a list of fields that comprise the user identity/credentials.
  2. Test the submitted data for the presense of the credential fields.
  3. If present, filter and pass the credentials to a data store adapter (or a validator). The chosen adapter needs to know how to extract the server-side values of the defined credentials fields.
  4. Compare the submitted credentials to the server-side credentials using the correct algorithm.
    1. If they don't match, or no credentials were presented, deny access to the resource.
    2. If they match, create a token to represent the user's authenticated identity.
  5. Store the token somewhere, such as in the session, if you don't want to do this workflow on each request.
  6. Also, [regenerating the session id] is recommended for "significant" changes to the trust "level" established between the server and user.

Subsequent Requests (using sessions)

Since credentials are not normally resubmitted by subsequent requests, Zend_Session uses the concept of "validating the session" to re-establish some-level of trust, in the absence of the original credentials. Clearly, applications should assign a lower trust "level" when authenticated credentials are not present in the current request. Some applications further reduce the trust level after a finite number of requests or time since credentials were last authenticated. Various validator plugins may be applied to impose conditions on validation of the session, often at some cost to convenience (e.g. locking sessions to an IP address).

  1. The session subsystem automatically retrieves data for this user, after applying any specified session validators.
  2. Check the session data for the presence of an auth token and credentials.
    1. If only a token is present, consider the request/session validated.
    2. If only credentials are present, perform the initial authentication.
    3. If both are present, then application specific behavior might include one of the above, or a hybrid, such as avoiding resetting the user's "login" time, but still escalating the current request's privileges (e.g. allow user to change their account password with this request).

Subsequent Requests (without sessions)

Some mechanism of associating users to an authorization or authentication identifier must exist, or subsequent requests would not know if the current user had previously authenticated successfully. Normally HTTP cookies are used for this purpose, since users often share IP addresses and other possible data provided with an HTTP request. Effectively duplicating Zend_Session is not in scope, and will not be considered further.

Credentials, Identity, and Tokens

Credentials are used to authenticate the user performing a request. However, the identity of a user might or might not be contained within the credentials. For example, the server-side data store might contain biometric data (e.g. fingerprints) so that simply supplying a fingerprint scan provides sufficient credentials to both authenticate and determine identity. Identity might equate to a single person, or even an abstract role granted to a set of individuals that satisfy the set of valid biometric data for a particular lab facility.

Authentication tokens represent the results of an authentication attempt, and vary from application to application. With Zend_Auth applications define their own Zend_Auth_Adapter to create an application-specific mechanism for authenticate($credentials), which also returns a token object that can respond to queries including isValid() and getIdentity(). As far as the authentication system is concerned, the application code may need to know:

  1. isValid() - whether or not a given set of credentials are valid
  2. getIdentity() - the user's identity (confirmed via authentication process)
  3. getMessage() - provides access to diagnostic messages generated during authentication

The application's definition of a user's identity can be arbitrarily complex, but none of that is relevant to the authentication code. Therefore, the authentication code needs a reference to the application's notion of an identity, but can treat it as an opaque value. Both "Identity" and "Credentials" must be fully defined before performing authentication so that authentication code can be assured it's talking about the same identity as the application, when it answers the question of whether a given identity is authenticated.

Authenticate Where?

Now that the basic concepts and terminology are established, there are several options to consider when integrating authentication processes into a ZF application.

  1. URL patterns, such as "/forum/admin_*/*"
  2. Module names, such as "/admin/post/edit"
  3. Controller names, such as "/forum/admin/topic/"
  4. Action names, such as "/forum/topics/admin/"
  5. Inside the business logic of an action controller

Note that each of these "locations" also presents an opportunity to use the authorization identity to determine access privileges and permissions. Authorization will be discussed in a later section.

For #1 above, the following code is inserted in Stage 2, just before the first dispatch().

The code above "protects" all URLs containing the pattern in "authenticate.URI" in our config.ini. Thus, this advantage counterbalances having "logic" outside of a regular controller. Flow of control continues after dispatch in the module and controller configured (default/LogonController):

To emphasize the distinctions between authentication and authorization, the ZFDemo application processes login credentials using a totally separate system from the forum module. The resulting authentication id must then be mapped to something usable by the forum module. Instead of creating a customized action controller subclass for this purpose, a form of the singleton pattern may be used, where a single "static" class is used as an "object" to support all forum action controllers with shared functionality.

Key Concepts: HTTP Authentication

The architecture needed to perform HTTP Authentication differs some from forms-based authentication. HTTP Authentication does not use a cookie to associate a visitor with a session containing authentication results (e.g. authentication or authorization id). The Zend Framework HTTP Authentication Adapter supports HTTP Authentication.

Key Parts

  1. The Authorization Header: This is a client request header sent with every request to resources inside the protected domains (list of URIs). This header contains the both the user's identity and their credentials, the format of which are specified by the RFC.
  1. Server-Side Data Store: Again, there are countless ways to store the credentials on/off the server. RFC 2617 defines what credentials are available and, to a certain extent, the format the credentials need to be stored in.

Workflow

This same workflow is performed on every request.

  1. Determine whether the client's Authorization header is formatted correctly and uses a supported authorization scheme (Basic or Digest).
    1. If not, return a 400 Bad Request response.
    2. Otherwise, pass the identity to a data store adapter. This adapter returns either the user's password (for Basic), or a password hash (for Digest). The data store adapter might implement a cache for performance reasons.
  2. Determine the type of auth
    1. If Basic auth, compare the submitted password to the looked up password
      1. - Allow access if they match
      2. - Return a 401 Unauthorized otherwise
    2. If Digest auth, use the looked up hash to calculate the server's digest value, and compare that to the client's digest value.
      1. - Allow access if they match
      2. - Return a 401 Unauthorized otherwise

At this point, you could create an identity token, but that need not be any of the authentication code's concern, since it's definition of user identity is fixed and defined by RFC 2617. Also, since there is no persistence via session, or any other mechanism, the whole concept of the cookie automatically authenticating the user for the rest of the session doesn't apply. The Authorization header effectively replaces the cookie and the session value.

The players in the HTTP Authentication scheme could probably be coerced into similar entities from the forms-authentication scheme, implying that the same actual API could be used to manage both processes. Given the differences in the work flow, and the fact that it has no relationship to sessions, there could be a reasonable argument against it. Zend_Auth itself actually provides very little value. About all it does anyway is automatically store the token returned from the adapter in the session, and provide methods to get the token. So there's no strong argument against just using the adapter by itself, which begs the question, should it just stand alone?


Next Section: 8. Access Control

Using ZF 1.0.3 I needed to do the following corrections to the files in section7_auth:

Set full read and write permission on folders:
section7_auth/data/
section7_auth/temporary/
section7_auth/temporary/sessions/

LOC: index.php
ERR: function name '_' not allowed by php
OLD: function _($msg)
NEW: search and replace all occurences of '_(' with '_x(' in all files in dir 'section7_auth/'!

LOC: index.php
ERR: local path doesn't work
OLD: //ZFDemoGrub('section7_auth', 'sandbox'); [comment out]
NEW: ZFDemoGrub('/Users/marcgrue/Sites/zfdemo/section7_auth/', 'sandbox'); [uncomment this line - use an absolute path]

LOC: index.php
ERR: php errors not showing for debugging (with my general php_ini settings)
NEW: ini_set('display_errors', true); [add this line before/after the line 'error_reporting(E_ALL|E_STRICT);']

LOC: section7_auth/bootstrap.php::stage1(), line 179
ERR: fatal error: Zend_View class declared twice
OLD: require 'Zend/View.php';
NEW: require_once 'Zend/View.php';

LOC: section7_auth/bootstrap.php::stage1() line 200
ERR: Going to 'Forums': wrong (empty) view object is rendered, so no topics are shown
NEW: $frontController->setParam('noViewRenderer', true); [add this line just before returning the $frontController]

LOC: section7_auth/bootstrap.php::stage2() ca line 354
ERR: '/index.php' added unnecessarily to baseurl
OLD: $baseUrl .= '/index.php';
NEW: $baseUrl .= '/';

LOC: section7_auth/default/controllers/IndexController.php::indexAction() line 29
ERR: '/index.php' added unnecessarily to baseurl
OLD: $this->view->baseUrl .= '/index.php';
NEW: //$this->view->baseUrl .= '/index.php'; [comment out]

LOC: section7_auth/forum/controllers/IndexController.php::redirectToTopics() line 120
ERR: link to forums is silently redirected back to 'home'
OLD: $this->setRedirectCode(303);
NEW: //$this->setRedirectCode(303); [uncomment]
ALTERNATIVE: add "/topics" to Forums link in zfdemo/index.php

LOC: section7_auth/forum/models/pdo/Topics.php::getPresentationModel() line 33
ERR: Zend_Date not loaded
NEW: require_once 'Zend/Date.php'; [add line]

LOC: section7_auth/forum/models/pdo/Posts.php::getPostsByTopicId() line 58
ERR: Zend_Date not loaded
NEW: require_once 'Zend/Date.php'; [add line]

sometimes the xpath function in line 96 of the _readFile function in Zend_Local_Data reads the data file
in a wrong way, so that a wrong path to an alias subnode is fetched. This causes:
"Fatal error: Method Zend_Date::__toString() must not throw an
exception in /Users/marcgrue/Sites/zfdemo/section7_auth/forum/views/scripts/topicsIndex.phtml on line 47"
By reloading the page a few times, the error disappears. I wonder how this happens? Is it some bug in
the xpath function? A dirty temporary solution to avoid the error is to comment out from line 166 to 193 in
Zend/Locale/Data.php - **NB** don't use this modified Zend library for other projects then!

LOC: Zend/Controller/Action.php function 'getViewScript' line 282
ERR: Exception: ERROR script 'admin-topicsIndex.phtml
OLD: $action = str_replace($this->_delimiters, '-', $action);
NEW: //$action = str_replace($this->_delimiters, '-', $action); [uncomment]
**NB**: dirty solution (don't normally modify Zend library files)!
The error is probabaly a consequence of $frontController->setParam('noViewRenderer', true); above

Hope this helps others to get this section of the tutorial going.