2. Authentication Modules

What’s An Authentication Module?

Authentication modules verify the identity of clients that connect to AMPS. Although secure verification systems can be complex in practice, verification is simple in principle. Verification relies on a secret piece of information, such as a password that is only known to one person. If the client successfully provides that information, the client has proved its identity. More complex systems may require that the server prove its identity to a client before the client provides the secret, and takes steps to help keep the secret safe during the authentication process.

The authentication module interface provides AMPS with a way of verifying the identity of clients that connect to AMPS.

When To Implement an Authentication Module

When your AMPS installation needs to verify the identity of users who connect to AMPS, you implement an authentication module. The AMPS provided default authentication modules provide default functionality that enables AMPS to run without requiring explicit authentication.

Authentication Context

AMPS uses authentication modules to identify clients when the client connects and logs on. Therefore, use of an authentication module takes place within a specific transport. The same module may have different configuration settings for different transport settings. For example, one port may be enabled to publish offers to AMPS, and may require one type of authentication, while a different port, and different type of authentication, may be used for subscribers to the offers.

Because the same module may be used with different parameters on different transports, AMPS allows you to save the parameters in an authentication context. AMPS creates an authentication context for each thread that listens for connections on a given transport.

Your module implements the amps_authentication_create_context function to create an authentication context. The function has this signature:

amps_authentication_context amps_authentication_create_context(amps_module_options options);

The authentication context is a pointer to data that your module defines. The AMPS server does not process or interpret this data. The server receives the pointer from your module when the module is initialized, and then provides the pointer back to your module for each authentication request.

If your module is unable to successfully create the context, you return a NULL context and use the amps_logger provided to your module to log information about the problem.

Notice that you cast the pointer to amps_authentication_context before you return it, and cast the pointer back to the type that your application uses after AMPS returns the pointer to you.

AMPS passes configuration options to modules as a pointer to an array of amps_option structs. AMPS indicates the end of the array with an option that has a NULL key. Your module processes options until it reaches an option with a NULL key. If no options are specified in the configuration file, the first amps_option in the array will have a NULL key.

The following snippet shows one way to process options:

for (; options->key != NULL; ++options) {
    // process the key/value pair here
}

When AMPS exits, AMPS calls amps_authentication_destroy_context with each of the authentication contexts created. This gives your module an opportunity to do any resource management, cleanup or logging necessary before AMPS shuts down. The function has this signature:

int amps_authentication_destroy_context(amps_authentication_context context);

Authenticating Users with AMPS

AMPS passes authentication requests to your module by calling amps_authenticate(). Your module implements an amps_authenticate function with the following signature:

int amps_authenticate(amps_authentication_context context,
                      const char*    user,
                      size_t         userLength,
                      const char*    passwd,
                      size_t         passwdLength,
                      char**         userOut,
                      size_t*        userOutLength,
                      char**         passwdOut,
                      size_t*        passwdOutLength);

The authentication context provided is the pointer returned by the call to amps_authentication_create_context() for the transport that received the request. The user and password passed in to the function are the credentials to authorize. Your function returns AMPS_SUCCESS if authentication succeeds, AMPS_FAILURE if authentication fails.

Your module can choose whether to provide the authenticated user name to clients that receive published messages. If your module sets the userOut parameter, AMPS uses the value in that parameter as the publisher for the connection. If you do not set this value, AMPS does not provide the user name with messages published.

Following is a simple, minimal implementation of amps_authenticate:

int amps_authenticate(amps_authentication_context context,
                      const char*    user,
                      size_t         userLength,
                      const char*    passwd,
                      size_t         passwdLength,
                      char**         userOut,
                      size_t*        userOutLength,
                      char**         passwdOut,
                      size_t*        passwdOutLength)
{
   // is_authorized (not shown) is a function that returns true if
   // the password provided is correct for the user provided

    if (is_authorized(user, userLength, passwd, passwdLength, context)) {
        // Copy the user provided to userOut. The module
        // can also choose not to provide a user name to AMPS by
        // skipping this step.

        // allocate space for the user name using the
        // amps_allocator saved during module initialization

        *userOut = (char*) amps_allocator(userLength+1);
        memset(*userOut+userLength, 0, 1);

        // Copy the user provided, set the length, and return.

        strncpy(*userOut, user, userLength);
        *userOutLength = userLength;
        return AMPS_SUCCESS;
    }
    else {
        return AMPS_FAILURE;
    }
}

AMPS also supports challenge/response authentication with the userOut and passwdOut parameters and the AMPS_RETRY status. To request that the client respond to a challenge, modules set the response in the userOut parameter, the challenge to be returned to the client in the passwdOut parameter and return an AMPS_RETRY status. AMPS returns this information to the client, which can then respond to the challenge in a subsequent requests.

Retrieving Additional Information

For some sites, a module may want or need additional information about the client logon request to be able to respond appropriately to the authentication request. For example, the module may want to reject requests from applications that don’t meet the client naming guidelines or requests that do not originate from an expected IP address.

Function Retrieves
amps_get_client_name The name of the current client as submitted in the logon request
amps_get_connection_name The connection name for the current client, as assigned by AMPS
amps_get_client_correlation_id The correlation ID for the current logon request, as submitted by the client in the logon request
amps_get_message_type The message type for the logon request.
amps_get_remote_address The address from which this connection was made.

These methods return a pointer to a NULL-terminated string. This string must not be modified by the module. The string is not guaranteed to be available after the module returns, so if the module wants to save the information returned by the string, the module must make a copy of the returned information.

Retrieving Headers from Websocket Connections

For connections that arrive over the websocket interface, AMPS makes any headers included in the handshake request available through the following function:

int amps_symbol_scope_get(const char* key, size_t keyLength,
                          const char** value, size_t* valueLength);

The amps_symbol_scope_get function retrieves the value of the header by setting the value to the the first character of the value, and setting the valueLength to the length of the value. Notice that this does not make a copy of the value, and the contents of the buffer are not guaranteed to be valid after the amps_authenticate function returns.

Important

The buffer that this function returns is managed by AMPS, and should not be freed by the module.

The buffer that this function returns is not guaranteed to be NULL-terminated.

For example, to retrieve the value of the X-Custom-Info header on a websocket authentication request, you might use code like the following:

const char* value       = 0;
size_t      valueLength = 0;

int rc = amps_symbol_scope_get("X-Custom-Info", strlen("X-Custom-Info"),
                               &value, &valueLength);

if (rc == AMPS_SUCCESS && value != 0 && valueLength != 0)
{
      /* use value & valueLength here */
}
else
{
     /* header is not available, fallback or fail request */
}

Tip

When building a module that uses this mechanism, keep in mind that HTTP headers are only provided for websocket connections: you may need to use a different mechanism (such as the logon correlation ID) for sending the equivalent custom information for connections that use a different protocol.

Managing Authentication Contexts

For authentication modules, AMPS creates a separate context for each processing thread for a Transport rather than a single context for each place the module is used in the configuration file. The number of processing threads created depends on the capacity of the system, and may change from release to release of AMPS. An authentication module must assume that an arbitrary number of contexts are created for each Transport.

This has the following implications:

  • State stored in an individual context is not shared across the Transport as a whole. For example, if your module needs to detect and prevent replay attacks, that state should be maintained in a global object (with appropriate locking) rather than being maintained in an individual context.
  • When a module uses external resources, such as a connection to an authentication server, carefully consider whether the resource is specific to the transport processing thread, specific to the transport, or specific to the instance. Manage the resource appropriately. For example, you might choose to have a single connection to an authentication server for the entire instance, or a connection per transport.

Caution

An authentication module must assume that an arbitrary number of contexts are created for each Transport.

One common approach to maintaining state is to provide a global data structure that stores shared state. For transport-specific state, the structure can provide lookup by transport name, with the name of the transport set on each context during context initialization, using the amps_get_transport_name utility function to find the transport name (see Utility Functions for the utility functions available).

Context Reset

AMPS provides the ability to reset the authentication contexts that provide service for a transport. When this occurs, for each context used by the transport, AMPS creates a new context and then destroys the old context.