Web security in Barracuda is built from three related components:
An authenticator is typically installed at a specific location in the web server and protects every URL below that location. For example, an authenticator installed at /private prevents unauthenticated users from accessing any URL below /private, even if the destination resource does not exist.
Authorization is optional. Some systems only need to authenticate a user and then allow that user to perform any operation in the protected realm. Most systems also need to check whether the authenticated user is allowed to perform the requested operation. If the user is authenticated but not authorized, the authorization logic returns a "403 Forbidden" response.
The Barracuda authenticators FormAuthenticator, BasicAuthenticator, and DigestAuthenticator implement the AuthenticatorIntf interface. AuthenticatorIntf provides a common API, so code that uses an authenticator does not need to know which concrete authenticator implementation is installed.
You can use an authenticator directly in a CSP page. Keep in mind that the CSP compiler compiles each CSP page into an HttpPage.
<html>
<body>
<h1>Welcome <%=user.getName()%></h1>
</body>
</html>
<%p
AuthenticatedUser* user =
auth.authenticate(request->getRequestURI(), request->getCommand());
if(!user) return; // Not authenticated. Response sent by BasicAuthenticator.
%>
<%! BasicAuthenticator auth; %>
<%!! auth(...); //Omitting actual arguments %>
<%g #include <BasicAuthenticator.h> %>
The code above works, but it is impractical because every CSP page would need to duplicate the authenticator setup. A better solution is to protect an HttpDir instance. An authenticator installed in an HttpDir protects all resources below that directory, including nonexisting resources.
You can create your own HttpDir class that replaces the HttpDir service method and validates users with an authenticator. In most applications this is unnecessary because Barracuda provides directory implementations that already support authentication. Typically, you install an authenticator in an instance of HttpResRdr, HttpResMgr, WebDAV, or another HttpDir-based object.
When a user is authenticated, the user's session contains an instance of the AuthenticatedUser class. C++ code can retrieve this object with AuthenticatedUser::get. C code can use AuthenticatedUser_get1. For example, a CSP page can check the current user as follows:
C++:
<%p
AuthenticatedUser *user = AuthenticatedUser::get(request);
if(user) { /* If authenticated */
}
%>
C:
<%p
AuthenticatedUser *user = AuthenticatedUser_get1(request);
if(user) { /* If authenticated */
}
%>
These C and C++ examples can be used by any CSP or HttpPage class, even if the page itself is not protected by an authentication directory. For example, one part of the Virtual File System may require authentication while another part is public. A public CSP page can still detect an authenticated user if that user first visited a protected part of the VFS.
Authorization is optional when using the authentication classes. When installing an authenticator in a directory object such as HttpResRdr, you may also pass an AuthorizerIntf implementation. AuthorizerIntf is an interface that your application implements to decide whether an authenticated user can access a resource.
If a protected realm requires authorization, the authenticator asks your AuthorizerIntf logic for permission after the user is authenticated. The authorizer receives the authenticated user, the HTTP method, and the relative resource path, then returns TRUE to allow the request or FALSE to deny it.
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. AuthorizerIntf
does not require a role model; that decision belongs to your application. You have three common options:
The last option requires a more complete AuthorizerIntf design, but it is also the most flexible. The example directory contains a security example that implements AuthorizerIntf with role mapping.
The following examples show how an authenticator and AuthorizerIntf are used. HttpDir, HttpResRdr, WebDAV, and similar directory types implement logic similar to the code below. The snippets are illustrative; they show what happens inside an HttpDir-based service function.
The following code fragment is from our security CSP page above:
AuthenticatedUser* user =
auth.authenticate(request->getRequestURI(), 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(relPath, 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);
}
MySecureDir inherits from HttpDir and replaces the HttpDir service function. The replacement service function first checks whether the user is authenticated. If authentication succeeds, it delegates the request to the original service function.
The code above lets any authenticated user perform any operation on the resources in MySecureDir. The authorizer's job is to analyze the request and check whether the authenticated user is allowed to perform the requested operation.
Assume that we have implemented MyAuthorizer, a class that implements AuthorizerIntf.
int
MySecureDir::service(const char* relPath, HttpCommand* cmd)
{
AuthenticatedUser* user = auth.authenticate(relPath, cmd);
if(!user)
return 0; // Not authenticated. Response sent by BasicAuthenticator.
// User is now authenticated, but is the user authorized?
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 MyAuthorizer to MySecureDir and use myAuthorizer.authorize to check whether the user is authorized. The example calls MyAuthorizer directly, but HttpDir, HttpResRdr, and similar classes use a pointer to the base interface, AuthorizerIntf. Any class that implements AuthorizerIntf must provide the authorize callback.
Before using the authenticator classes, you must provide a small amount of support code. The minimum requirements are a user database interface and a response handler. The response handler sends the login page for form-based authentication and sends an error response when authentication fails.
The simple security example below implements two classes: one class for the user database API and one class for the login response.
UserIntf is an interface that must implement the getPwd callback. FormAuthenticator, BasicAuthenticator, and DigestAuthenticator call this function when validating the username 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 callback. Barracuda does not use C++ virtual functions for this interface because the core library is designed in C. Interface constructors receive function pointers, so the UserIntf constructor takes getPwd as an argument. In C++ this callback must be declared static; inside the callback, cast the interface pointer back to the derived class if you need access to object state.
void MyUserDB::getPwd(UserIntf* super, AuthInfo* info)
{
MyUserDB* o = (MyUserDB*)super; //Cast from base class. o equals this ptr
(void)o; // Not used in this simple example.
if(!strcmp(info->username, "admin"))
{
strcpy((char*)info->password, "admin");
}
}
Our getPwd callback is intentionally simple. It checks whether the username is "admin" and sets the expected password to "admin". The callback then returns control to the authenticator, which compares the password stored 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 more complex example of a custom-made database can be found in the security example.
AuthInfo is a stack-created container used by the authenticator classes. Barracuda passes this object to the user-provided callbacks. It carries information from the authenticator to the callback, and it lets callbacks return information to the authenticator and to other callbacks.
LoginRespIntf is an interface that must implement the service callback. BasicAuthenticator and DigestAuthenticator call this callback when a user fails to log in. FormAuthenticator also calls it when the client needs the form login page.
struct MyLoginResponse : public LoginRespIntf
{
MyLoginResponse() : LoginRespIntf(service) {}
private:
static void service(LoginRespIntf* super, AuthInfo* info);
};
MyLoginResponse follows the same pattern as MyUserDB. Its constructor initializes the base interface. The LoginRespIntf constructor requires a function pointer to the response service callback.
void
MyLoginResponse::service(LoginRespIntf* super, AuthInfo* info)
{
info->cmd->response.write("Invalid username/password combination");
}
The example service method above is intentionally minimal. It assumes the callback is called only when the user failed to log in, so it only works correctly with Basic and Digest authentication.
The example does not set the response code or content type. The content type should be set to "text/plain" because the response is not HTML. The response code is set automatically by the authenticators: 401 for Basic and Digest authentication, and 200 for form-based authentication. See RFC 2616 section 10 for more information about HTTP status codes.
Since the simple security example uses form-based login, the response service method must distinguish between sending the login form and sending a login failure page.
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 access to the HttpCommand object, including its HttpRequest and HttpResponse members. This means the callback can use the request and response APIs directly.
For form-based login, it is usually best to send HTML pages. The callback can forward the request to either a static HTML page or a CSP page. The example checks the information in AuthInfo and forwards the request to either the login page or the error page.
Notice that the login pages use the .shtml extension. This extension makes the pages hidden from normal browser requests. Files with the .shtml extension can only be accessed by HttpResponse::include and HttpResponse::forward.
The example still does not set the content type explicitly. This is not necessary when using HttpResponse::forward because the forwarded page sets it automatically if it has not already been set. The content type is set to "text/html" because the forwarded resource uses the .shtml extension.
We have now completed the support code needed for authentication. The next example assembles a simple web server protected by a form authenticator:
int main(int argc, char* argv[])
{
/********** 1 *************/
ThreadMutex m; // Protects the dispatcher and server.
SoDisp dispatcher(&m); // The socket dispatcher.
// Create the Web Server object and bind it to the dispatcher.
HttpServer server(&dispatcher);
HttpServCon serverCon(&server, // Create a listen object that
&dispatcher, // listens on port 80.
80);
/********** 2 *************/
MyUserDB userDB;
MyLoginResponse loginResp;
FormAuthenticator authenticator(&userDB, "Barracuda", &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.
Section 2 creates MyUserDB and MyLoginResponse, then initializes FormAuthenticator with pointers to their base interfaces and the realm string.
Section 3 creates an HttpResRdr instance that reads from the hard drive through DiskIo. The HttpResRdr does not need a directory name because it is used as a web server root directory. The example then inserts the HttpResRdr instance as a root directory in the web server.
Section 4 assigns the form authenticator to the HttpResRdr instance. The AuthorizerIntf argument defaults to NULL and is not set because this example has not implemented an authorizer yet. The authenticated "admin" user is therefore allowed to perform any operation on any resource in this HttpResRdr instance.
So far, the example requires authentication, but authenticated users can perform any HTTP method on all resources in the HttpResRdr resource collection.
Authenticated users can optionally be authorized. We set the authorizer to NULL in this example.
AuthorizerIntf is an interface that must implement the authorize callback.
struct MyAuthorizer : public AuthorizerIntf
{
MyAuthorizer() : AuthorizerIntf(authorize) {}
static BaBool authorize(AuthorizerIntf* super,
AuthenticatedUser* user,
HttpMethod method,
const char* path);
};
BaBool MyAuthorizer::authorize(AuthorizerIntf* super,
AuthenticatedUser* user,
HttpMethod method,
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 by HttpDir, HttpResRdr, WebDAV, or another protected directory object. The authorize method accepts or denies the request by returning TRUE or FALSE.
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 protected directory node in the Virtual File System. For example, assume that an HttpResRdr is installed at /MyResourceReader with an authenticator, and the user requests /MyResourceReader/MyDir. The path argument passed to the authorizer is then MyDir.
The HTTP method argument is useful when a user's privilege level depends on the requested operation. For example, if GET is used for reading and PUT is used for writing, an AuthorizerIntf implementation can allow many users to read while restricting PUT to selected users.
Please see the security example for more information on how to implement an authorizer.
Barracuda includes support for tracking unsuccessful and successful login attempts. The LoginTracker class requires some application support code.
You also need LoginTracker if you want to add session attributes or change session settings immediately after a user logs in.
The following diagram shows the authenticator classes and how they relate to each other.
The authentication and authorization code consists of a number of
classes. HttpSession, HttpSessionAttribute, HttpDir, and HttpResRdr are part of
the web server code, not the authenticator implementation. They are
included in the diagram to clarify the relationships between the classes.
The class and interface boxes in the diagram are clickable. Select any box to open the API reference documentation for that element.