(Originally a message from Geert Bevin on the RIFE users' mailing list)
Authentication basically is built on RIFE's stateful Aspect oriented technique that I call behavioral inheritance (and more recent threedimensional flow). This concept is sadly under documented and my books will take care of that in a month or two. What happens is that you put one site structure on top of another one, and you declare variables that need to be watched. When RIFE hits the upper layer, it will detect that the request was intended for the lower layer and preserve all its data over any number of subsequent requests. The logic in the upper layer executes until one of the watched variables gets a value. Then the childTriggered method is executed and your code is able to evaluate the value to indicate whether the logic should stay in the top layer, or if it should fall through and restore the original request.
All what RIFE's authentication relies on, is a string authentication ID, since that's what need to work with child triggers. See the SessionManager Javadoc
.
Now RIFE also has a collection of authorization features that rely on Credentials and a CredentialsManager. Credentials are actually an empty interface, they can contain anything, as long as the associated CredentialsManager is able to verify if they are correct and return a user ID. RIFE provides one sub-interface of Credentials, RoleUserCredentials and one implementation of this, RoleUser. The associated manager classes implement the CredentialsManager interface, but also the RoleUserManager interface. The latter provides a lot of additional functionality to actually maintain the users in the back-end store. The rest are concrete memory or database implementations of the managers.
The last part is SessionValidator, it is essentially a bridge between a CredentialsManager and a SessionManager. The validity of a session is often dependent on external attributes which define the context for a valid session that goes beyond a valid session ID. This interface allows you to write optimized queries to speed up the validation of an active authentication session (ie. a single database query instead of having to use the separate manager classes to achieve this). You can find the explanation of it in the SessionValidator Javadoc
.
Flow of execution
Note: This is a work in progress.
Here's a simple authenticated site declaration.
<element id="AuthElement" extends="rife/authenticated/memory.xml">
<property name="template_name">auth.login</property>
<property name="role">admin</property>
<property name="authvar_type">cookie</property>
<!-- The fields on the login form need to be in a "credentials" submission,
since that's what RIFE's built-in authentication elements will read
to construct the Credentials object for verification. -->
<submission name="credentials">
<param name="login"/>
<param name="password"/>
<param name="remember"/>
</submission>
<!-- If a valid "authid" cookie is present, fall through to the element
that extends this one (i.e., allow access to the protected page.) -->
<childtrigger name="authid"/>
<incookie name="authid"/>
<outcookie name="authid"/>
<!-- Allow the authentication elements to set an additional cookie for
constructing a new login session without prompting for a password. -->
<incookie name="rememberid"/>
<outcookie name="rememberid"/>
</element>
<group inherits="AuthElement">
<element id="MyAccount" implementation="com.foo.MyAccount" url="myaccount"/>
</group>
Now let's trace a few requests through the authentication system to see how RIFE actually does the authentication.
Our first request is an anonymous user, with no rememberid or authid cookies.
- Since the MyAccount element is in a group or subsite that inherits from another element, the parent element is invoked to handle the request first. AuthElement is implemented by RIFE's PurgingMemoryAuthenticated
class, which has a number of superclasses: MemoryAuthenticated
, RoleUserAuthenticated
, Authenticated
, Identified
, Element
, and finally ElementSupport
.
- There is a <childtrigger> tag in the AuthElement declaration, so RIFE looks for the input named by the tag, in this case a cookie named authid. There isn't one, so AuthElement is processed as usual (its processElement() method is called.)
- The implementation of processElement(), which is shared by all of RIFE's built-in authentication elements, is in the Authenticated
class. It looks for a "remember me" cookie. This cookie's name is set by the remembervar_name property; the default value is rememberid. In this case there is no such cookie.
- Next it looks for a submission with the name given by the submission_name property; the default is credentials. In this case there is no submission.
- Finally, it prints the template named by the template_name property, in this case auth.login. That template typically contains a login form.
The user fills in the login form with an incorrect password. The login form submits to the same URL that it originally came from; the same authentication element that displayed the form will handle the submission.
- Once again, control is first given to the parent element, AuthElement.
- There is still no authid cookie, so control is passed to Authenticated.processElement().
- There is still no rememberid cookie.
- But this time there is a submission called credentials. The contents of the submission are fetched as a submission bean, whose class is determined by the particular subclass of Authenticated
that's being used. The class must implement the Credentials
interface. In this case, since our authentication element is a subclass of RoleUserAuthenticated
, the RoleUserCredentials
class is used. TO DO: document AuthenticatedDeployer mechanism and how it determines the credentials class
- The validate() method of the Credentials
object is called to ensure that the user entered syntactically valid information. Validation capability is required for credentials objects; the Credentials
interface is a subinterface of the Validated
interface used by RIFE's validation system. In this case, the user didn't enter the correct password, but the password isn't invalid input per se, so the validation succeeds.
- Next, the authentication element's SessionValidator
is queried for its CredentialsManager
. The session validator in this case is MemorySessionValidator
and it returns a MemoryUsers
instance. TO DO: document how the session validator and credentials managers are chosen
- The credentials manager is the class that's actually responsible for verifying a user's password. Its verifyCredentials() method is called. In this case the password is incorrect, so the method returns -1.
- The login template is displayed again.
Now the user supplies the correct password and leaves the "Remember Me" checkbox unchecked.
- Once again, control is first given to the parent element, AuthElement.
- There is still no authid cookie, so control is passed to Authenticated.processElement().
- There is still no rememberid cookie.
- There is a credentials submission.
- The credentials validate successfully.
- The credentials manager is called to verify the password. In this case the password is correct, so the method returns the user's positive user ID.
- The credentials object is checked to see if it implements the RememberMe
interface. The default credentials class does, so the "Remember Me" setting is fetched. In this case it's set to false.
- The startNewSession() method is called.
- The user did not check the "Remember Me" box, so startNewSession() doesn't try to create a new remember ID.
- The authentication element's SessionValidator
is queried for its SessionManager
. In this case the session manager is a
MemorySessions
.
- The session manager's startSession() method is called. It is passed the user ID (from the credentials manager), the client IP address, and a flag indicating whether the session was restored from a "remember me" cookie (false in this case). If the session manager is satisfied with its inputs, it returns an authentication session ID, which is just a unique string identifier. The built-in session managers use the UniqueIdGenerator
class to generate their authentication session IDs.
- Since the authvar_type of this element is set to cookie, startNewSession() sets the cookie (authid, the default name) to the new authentication session ID. It does this by calling the setCookie() method defined by its ElementSupport
superclass.
- The element's setCookie() method delegates to the setCookie() method in the ElementContext
instance that the Web engine assigns to each invocation of an element. TO DO: link to documentation on element context. Maybe already discussed in the docs somewhere?
- The element context's setCookie() does more than just set the cookie: if the current element has a child element, it checks to see if the cookie name matches the child trigger. If so, it calls the element's childTriggered() method. Since the cookie name matches, that happens in this case.
- TO DO: document childTriggered() flow
- The childTriggered() method returns true to indicate that the child was successfully fired. Control returns to the element context (which, you will recall, called childTriggered() while setting the authentication session ID cookie). The element context, seeing the true return value, throws a ChildTriggeredException
, which aborts the authentication element's processElement() call and ends the request.
Now the user reloads the page.
- TO DO: document childTriggered() flow of control
The user goes away for a while, long enough that his session expires, then tries to reload the page again.
- TO DO: document childTriggered() with expired/invalid session
Not wanting to be asked to log in again, the user checks the "Remember Me" checkbox and supplies his username and password.
- TO DO: document creation of remember-me session, including RememberManager lookup
Now the user goes away again and reloads after his session has expired.
- TO DO: document reestablishment of session with rememberid
Hooks
The authentication system has hooks that user-written authentication elements can use to intercept the flow of execution. These are all methods defined in the Authenticated
class; their default implementations are all empty.
| Method |
When it's called |
| initializeAuthentication |
At the start of processElement(). |
| entrance |
After the template instance has been instantiated. |
| unvalidatedCredentials |
On login form submission when the credentials object's validate() method fails. |
| validatedCredentials |
On login form submission when the credentials object's validate() method succeeds, but before the credentials manager verifies them. |
| acceptedCredentials |
After the credentials manager verifies that the credentials are valid. |
| refusedCredentials |
After the credentials manager rejects the supplied credentials. |
| authenticated |
After a new authentication session has been successfully created. |
| sessionCreationError |
When the session manager couldn't create a new authentication session. |
| sessionNotValid |
When a session manager doesn't accept a provided authentication ID that a user provides after having been logged in. |
Both refusedCredentials and sessionCreationError have default implementations that set the appropriate validation errors in the template, so if you want to preserve this, you need to call the super method in your own version.