Authentication and Authorization

Web security is divided into a number of components.

An authenticator is typically installed at a certain location in the web-server and covers any URL that crosses into the protected realm. As an example, an authenticator may be installed at '/private'. The authenticator makes sure no user can access any URL below '/private' without first being authenticated, even if the destination resource does not exist.

Authorizing users is optional in Barracuda. It may be sufficient to authenticate a user and then let the authenticated user perform any kind of operation on the server. However, most systems will require that the user is also authorized to perform the requested operation. Authorizing users is typically based on the path element of the URL. For example, an authenticated user may not have access to perform a certain operation. The authorization logic will in this case return a "403 Forbidden" response message to the client.

Authenticating Users

The Barracuda authenticators FormAuthenticator, BasicAuthenticator, and DigestAuthenticator are implementing the abstract class AuthenticatorIntf. The AuthenticatorIntf provides a common API, and code using the authenticators does not need to know the actual authenticator implementation.

You can directly use an instance of one of the above authenticators in, for example, a CSP page. Keep in mind that a CSP page is compiled into a HttpPage by the CSP compiler.

<html>
<body>
<h1>Welcome <%=user.getName()%></h1>
</body>
</html>
<%p
   AuthenticatedUser* user = auth.authenticate(request->getCommand());
   if(!user) return; //Not authenticated. Response sent by BasicAuthenticator.
%>
<%! BasicAuthenticator auth; %>
<%!! auth(...); //Omitting actual arguments %>
<%g #include <BasicAuthenticator.h> %>

The above code works, but is impractical to use as each CSP page would have to duplicate the authenticator code. A better solution is to protect a HttpDir instance since an authenticator installed in a HttpDir instance protects all resources below the directory, including nonexisting resources.

You can create your own HttpDir class that overloads the HttpDir service method and validates the users by using an authenticator as shown in the above CSP example; however, this is normally not required as Barracuda provides a number of HttpDir implementations that does this for you. One typically inserts an authenticator instance into an instance of HttpResRdr, HttpResMgr, WebDAV, etc.

Using the AuthenticatedUser Object

When a user is authenticated, the user's session object contains an instance of the AuthenticatedUser class. One can get a reference to this object by calling AuthenticatedUser::get. As an example, one can get the object in a CSP page as follows:

C++:

<%p
AuthenticatedUser *user = AuthenticatedUser::get(request);
if(user) { /* If authenticated */
}
%>

C:

<%p
AuthenticatedUser *user = AuthenticatedUser_get(request);
if(user) { /* If authenticated */
}
%>

The above C and C++ example may be used by any CSP and/or HttpPage class even if the CSP or HttpPage object is not protected by an authentication directory. For example, you may have a part of the virtual file system that requires authentication and one part that does not require authentication. A CSP page in a public part of the virtual file system can detect if a user is authenticated if the user first visited the part of the file system that requires authentication.

Authorizing Users

Authorizing a user is an optional feature when using one of the authentication classes. You can optionally pass in the reference to a AuthorizerIntf interface class when you insert an instance of one of the authentication classes into, for example, a HttpResRdr instance. The AuthorizerIntf interface class is an abstract class that you must implement. We will explain how to implement a AuthorizerIntf class later.

If you want user authorization in a realm, you must implement the functionality needed by the AuthorizerIntf interface class. The AuthorizerIntf interface class is the interface between the authenticator classes and the Security Realm. This is an abstract class that you must implement. When a user is authenticated, the authenticator object queries your Security Realm logic for permission to access the requested resource.

Many modern enterprise servers group users into roles. A role is an abstract grouping of users. You can think of a role as a group. As such, a user can fill one or more roles. The AuthorizerIntf interface class does not dictate whether to implement roles or not -- it is your decision. You basically have three options:

The last option requires a more complex design of your AuthorizerIntf, but is also the most flexible solution. The example directory contains a security example that implements AuthorizerIntf from scratch with roles mapping.

How the Authenticators are Used

The following examples shows how the authenticator and AuthorizerIntf are used. The HttpDir, HttpResRdr, WebDAV, etc. implements code similar to the code below. Thus the following code is only meant to illustrate how the authenticator and authorization logic is used by HttpDir types.

The following code fragment is from our security CSP page above:

AuthenticatedUser* user = auth.authenticate(request->getCommand());
if(!user)
   return; //Not authenticated. Response sent by BasicAuthenticator.

The above code can, for example, be used in an extended HttpDir service function. Assume that we have implemented a MySecureDir class that inherits from HttpDir.

int MySecureDir::service(const char* relPath, HttpCommand* cmd)
{
   AuthenticatedUser* user = auth.authenticate(cmd);
   if(!user)
      return 0; // Not authenticated. Response sent by BasicAuthenticator.

   //User is now authenticated

   //Delegate request to original service function.
   return o->orgService(this, relPath, cmd);
}

Our MySecureDir class inherits from HttpDir and overloads the HttpDir service function. The overloaded service function first checks if the user is authenticated. The service function then delegates the request to the original service function if the user is authenticated.

The above code will let any authenticated user perform any operation on the resources in MySecureDir. The job of the authorizer is to analyze the request and check if the authenticated user is allowed to perform the requested operation.

Assume that we have implemented MyAuthorizer, a class that implements the AuthorizerIntf.

int
MySecureDir::service(const char* relPath, HttpCommand* cmd)
{
   AuthenticatedUser* user = auth.authenticate(cmd);
   if(!user)
      return 0; // Not authenticated. Response sent by BasicAuthenticator.

   //User is now authenticated, but is he authorized to perform the operation?
   if(myAuthorizer.authorize(user, cmd->request.getMethodType(), relPath))
   {
      //User is authorized.
      //Delegate request to original service function.
      return o->orgService(this, relPath, cmd);
   }
   // else not authorized
   cmd->response.sendError(403); //Not authorized.
   return 0; // Response sent. Stop searching for duplicate directories.
}

We have added the MyAuthorizer class to our MySecureDir class, and we are using the method authorize in the instance variable myAuthorizer to check if the user is authorized. We are directly using the MyAuthorizer class, but the HttpDir, HttpResRdr, etc, are using a reference to the base class -- the AuthorizerIntf class. This class defines one method, the authorize method, which must be implemented by any class that inherits from the AuthorizerIntf class.

Implementing the Login Support Code

We must implement a number of support classes/functions before we can use the authenticator classes. The minimum requirements are a user database and code for sending response messages to the server. The response is sent if the user fails to login or when sending the initial login page when using form based login.

We need to implement two classes for our simple security example: one class for the database API and one class for handling the response message.

Implementing UserIntf

The UserIntf is an abstract class that we must inherit from and implement the "GetPwd" method. The "get password" method is used by FormAuthenticator, BasicAuthenticator, and DigestAuthenticator when validating the user and password provided by the client.

class MyUserDB : public UserIntf
{
   public:
      MyUserDB() : UserIntf(getPwd) {}
      static void getPwd(UserIntf* intf, AuthInfo* info);
};

Our user database inherits from UserIntf and implements the 'getPwd' method. We explained in the Barracuda introduction that Barracuda is not using virtual functions for C++ as the Barracuda code is designed in C code. The abstract classes in Barracuda expect function pointers as argument. The UserIntf constructor takes getPwd as argument. This works since we have declared getPwd to be a static C++ method. The drawback of using callbacks in C++ is that we must manually cast and explicitly use the 'this' pointer.

void MyUserDB::getPwd(UserIntf* super, AuthInfo* info)
{
   MyUserDB* o = (MyUserDB*)super; //Cast from base class. o equals this ptr
   if(!strcmp(info->username, "admin"))
   {
      info->password = info->userName;
   }
}

Our "get password" callback method is simple and will be much more complex in a real application. The method checks if the username is "admin" and sets the expected password to be "admin". The method then returns control to the authenticator that called the get password method. The authenticator compares the password we set in AuthInfo with the password provided by the client.

Barracuda allows you to either implement your own user database or interface a database to an external program. A database can be as simple as the one in this example which is copied from the SimpleDebugger C++ example. A more complex example of a custom made database can be found in the security example.

AuthInfo is a container class that is created on the stack in the authenticators. AuthInfo is used in all the user provided interface callback methods used by the authenticator classes. The class provides the callbacks with information from the authenticator. The class is also used for sending information back to the authenticator and for sending information to the the other callbacks.

Implementing LoginRespIntf

The MyLoginResponse is an abstract class that we must inherit from and implement the "service" method. The "service" method is used by BasicAuthenticator and DigestAuthenticator when a user fails to login. The service method is also called when the FormAuthenticator needs to send the form login page to the client.

struct MyLoginResponse : public LoginRespIntf
{
      MyLoginResponse() : LoginRespIntf(service) {}
private:
      static void service(LoginRespIntf* super, AuthInfo* info);
};

The MyLoginResponse is similar to our MyUserDB constructor. The constructor initializes the abstract base class. The base class constructor LoginRespIntf requires a function pointer to the LoginRespIntf service method.

void
MyLoginResponse::service(LoginRespIntf* super, AuthInfo* info)
{
   info->cmd->response.write("Invalid username/password combination");
}

The above example service method is very naive. The service method assumes that the method is called if the user failed to login. Therefore, this service method only works with Basic and Digest authentication.

You may have noticed that we did not set the response code or the content-type. The content type above should have been set to "text/plain" as we do not send back HTML. The response code is automatically set by the authenticators. The HTTP response code is set to 401 for basic and digest authentication, and 200 for form based authentication. See RFC 2616 section 10 for more information on the HTTP status codes.

We need to rewrite the response service method since we will use form based login in our simple security example.

void MyLoginResponse::service(LoginRespIntf* super, AuthInfo* info)
{
   if(info->username) // If login failed
   {
      //Delegate to the error page.
      info->cmd->response.forward("/loginfailed.shtml");
   }
   else // Form login requesting the form page
   { 
      //Delegate to the form login page.
      info->cmd->response.forward("/formlogin.shtml");
   }
}

The response service method has full access to the HttpCommand object, the HttpRequest, and HttpResponse container class. This means that we can use any of the API's provided by HttpRequest and HttpResponse object.

We want to send back HTML pages for our form based login. It is extremely convenient to simply forward the request to either a static html page or a CSP page. We analyze the information in AuthInfo and forward the request to either the login page or the error page.

Notice that we use the .shtml extension for our login pages. The .shtml extension makes these pages hidden for normal requests. Files with the .shtml extension can only be accessed by method HttpResponse::include and HttpResponse::forward.

You may have noticed that we are still not setting the content-type. This is not necessary when using HttpResponse::forward since the page we forward the request to will do this automatically if not already set. The content-type is set to "text/html" since we are forwarding to a resource with the .shtml extension.

Simple Test of Our Login Support Code

We have now completed the necessary support code for authenticating users, and we can now assemble a simple web-server that is protected by an authenticator:

int main(int argc, char* argv[])
{
   /********** 1 *************/
   ThreadMutex m; //Create the mutex protecting the dispatcher and server.
   SoDisp dispatcher(&m); //Create the socket dispatcher object.
   //Create the Web-Server object and bind it to the dispatcher.
   HttpServer server(&dispatcher);
   HttpServCon serverCon(&dispatcher, // Create a listen object that
                         &server,     // listens on port 80.
                         80);

   /********** 2 *************/
   MyUserDB userDB;
   MyLoginResponse loginResp;
   FormAuthenticator authenticator(&userDB, &loginResp);


   /********** 3 *************/
   DiskIo io;   
   HttpResRdr rootDir(&io, NULL); //Name not needed for root directory.
   server.insertRootDir(&rootDir);

   /********** 4 *************/
   rootDir.setAuthenticator(&authenticator);
   dispatcher.run(); //Never returns.
} 

The first section above is identical to the example in the Barracuda introduction.

We create an instance of our MyUserDB and MyLoginResponse in the second section. We then initialize the FormAuthenticator instance by passing in a reference to the super classes for MyUserDB and MyLoginResponse.

We create a HttpResRdr instance that is reading from the hard drive in the third section above. We do not need to give the HttpResRdr a directory name as it will be used as a web-server root directory. We then insert the HttpResRdr instance as a root directory in the web-server.

We assign the form authenticator to the HttpResRdr instance in section 4. The AuthorizerIntf argument, which defaults to NULL, is not set since we have so far not implemented an authorizer. This means that our "admin" user is allowed to perform any operation on any resource in the HttpResRdr instance.

Implementing AuthorizerIntf

We have thus far designed a web-server that requires authentication, but authenticated users are allowed to perform any HTTP command on all resources in the HttpResRdr resource collection.

Authenticated users can optionally be authorized. We set the authorizer to NULL in the above example.

The AuthorizerIntf is an abstract class that we must inherit from and implement the "authorize" method.

struct MyAuthorizer : public AuthorizerIntf
{
      MyAuthorizer() : AuthorizerIntf(authorize) {}
      static BaBool MyAuthorizer::authorize(AuthorizerIntf* super,
                                            AuthenticatedUser* user,
                                            HttpRequest* req,
                                            const char* path);
};

BaBool MyAuthorizer::authorize(AuthorizerIntf* super,
                               AuthenticatedUser* user,
                               HttpRequest* req,
                               const char* path)
{
   MyAuthorizer* o = (MyAuthorizer*)super; //Cast from base class
   if(......) //Add code here
      return TRUE;
   return FALSE;
}

The authorizer is called after the user is authenticated from HttpDir, HttpResRdr, WebDAV, etc. We explained how this works in How the Authenticators are Used above. The authorize method should either deny or accept the request by returning FALSE or TRUE to the caller.

The user argument is the current user. You can use this argument when checking if the current user is authorized to perform the requested operation.

The path argument is the relative path from the Authenticate directory node in the virtual file system. For example, assume that a HttpResRdr is installed at location /MyResourceReader with an authentication class and the user requests the path /MyResourceReader/MyDir. The path argument will then be MyDir.

The request object is useful when designing a system where the user's privilege level depends on which HTTP method is being used. Say GET is used for reading and PUT is used for writing; a AuthorizerIntf could then restrict the use of PUT to only one user.

Please see the security example for more information on how to implement an authorizer.

Tracking Users

Barracuda comes with support for tracking unsuccessful and successful login attempts. The LoginTracker class requires that you write some support code.

You also need the LoginTracker if you would like to add session attributes or change any of the settings in the session as soon as a user logins.

The Authenticator Classes

The following diagram shows the authenticator classes and how they relate to each other.

The authentication and authorization code consist of a number of classes. The HttpSession, SessionAttribute, HttpDir, and HttpResRdr are part of the web-server code, but are not part of the authenticator. They are included in the diagram below to clarify the relations between the various classes.


Drop-in Solution

You have the option of creating your own authenticator logic or using one of the drop-in solutions.

Creating your own authenticator and authorizer gives you the advantage of customizing your solution, but prior to making your own code, consider the following:

Barracuda provides a drop-in solution for authenticating, authorizing, and administrating users and constraints.

TBD