Design Patterns for Secure Ice Applications

There are many ways to design secure applications with Ice. Most of the principles that apply to traditional web applications can be transposed to Ice applications (session based authentication or token based authentication for example). Unlike HTTP which uses non-persistent connections, Ice also supports persistent connections that can be used for security as well.

We will explore in this article some common approaches for building secure Ice applications.

On this page:

Token based security

Security based on tokens is quite common with web applications: the client sends credentials to the server which in turn replies with a token. The token is opaque to the client and contains information used by the server to check whether the client has permission to access a resource. The advantage of this approach is that it doesn't require the server to maintain state for the client. The generated token can be used in any server to verify permissions to access a resource.

Below is a Slice interface which uses explicit tokens to verify permissions of a user to add or remove items from a cart:

Slice
interface SecurityTokenFactory
{
    string createSecurityToken(string userId, string password);
}
 
exception PermissionDeniedException
{
    string reason;
}

interface CartManager
{ 
    void addItem(string itemId, int quantity, string token) throws PermissionDeniedException;
    void removeItem(string itemId, string token) throws PermissionDeniedException;
}

The client application first requests a token from the server by calling createSecurityToken on the security token factory. The returned token is then used to call addItem or removeItem to modify the user's cart. The implementation of these two operations checks the token to make sure it's valid. An exception is thrown if the token check in the server fails.

The authentication in this example is based on a user identifier and password but it could also be based on any other type of credentials such as certificates (certificates would typically be provided as a sequence of bytes). If the client uses a X.509 certificate to establish the SSL/TLS connection with the server, the authentication could use the identity (distinguished name) contained in the client's certificate.

Server implementation

A sample Java implementation of the cart manager is shown below:

Java
class SecurityTokenFactoryImpl implements Demo.SecurityTokenFactory
{
    public String createSecurityToken(String userId, String password, com.zeroc.Ice.Current current)
    {        
        return generateToken(userId, password); // Check the userId/password and generates a token.
    }
}

class CartManagerImpl implements Demo.CartManager
{
    public void addItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException
    {
        checkToken(token); // Ensures the given token is valid
 
        String userId = getUserIdFromToken(token);
        java.util.Map<String, Integer> cart = _carts.get(userId);
        if(cart == null)
        {
            cart = new java.util.HashMap<String, Integer>()
            _carts.put(userId, cart);
        }
        cart.put(itemId, quantity);
    }
 
    public void removeItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException        
    {
        checkToken(token); // Ensures the given token is valid
 
        java.util.Map<String, Integer> cart = _carts.get(getUserIdFromToken(token));
        if(cart != null)
        {
            cart.remove(itemId);
        }
	}
 
    // A map of userId -> Cart where Cart is a map of item ID -> Quantity
    private java.util.Map<String, java.util.Map<String, Integer>> _carts;
}

We implement the interfaces Demo.SecurityTokenFactory and Demo.CartManager that were generated for the Slice interfaces. The createSessionToken method calls a generateToken method with the credentials. This method returns a string token if the user credentials are valid. The token is an encrypted representation of various data, such as the user ID and a timestamp that limits the lifespan of the token. The server later retrieves this data by decrypting the token.

Showing the token generation is out of the scope of this article. The token generation and verification is typically handled by a 3rd party library.

The addItem and removeItem methods are used to modify the user's cart. The token is explicitly provided to each call and the server calls checkToken to ensure its validity. Again, the implementation of checkToken isn't shown here but typically this method decrypts the token data, verifies it's still valid and checks the permissions of the user. Since the token includes the user identifier, we can extract it from the token to retrieve the user's cart and add or remove items.

Here's the main method to demonstrate how to create an Ice communicator, Ice object adapter and then register both servants:

Java
public static void main(String args[])
{
    com.zeroc.Ice.Communicator communicator = com.zeroc.Ice.Util.initialize(args);
    com.zeroc.Ice.ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints("CartAdapter", "ssl -p 10000");
    adapter.add(new SecurityTokenFactoryImpl(), new com.zeroc.Ice.Identity("SecurityTokenFactory", ""));
    adapter.add(new CartManagerImpl(), new com.zeroc.Ice.Identity("CartManager", ""));
    communicator.waitForShutdown();
}

Additional configuration for the SSL/TLS transport (such as the server certificate) needs to be provided through an Ice configuration file. The endpoint defined above allows a client to access the server with the SSL/TLS transport on port 10000. Since no -h is specified, the server will listen to all the network interfaces available on the host computer.

Client implementation

Using the cart manager service is straightforward with Ice.

You will find below a sample client which obtains a security token and then adds and removes an item from the cart by calling on the cart manager Ice object:

Java
public static void main(String args[])
{
    com.zeroc.Ice.Communicator com = com.zeroc.Ice.Util.initialize(args);
    Demo.SecurityTokenFactoryPrx factory = Demo.CartManagerPrx.uncheckedCast(com.stringToProxy("SecurityTokenFactory:ssl -p 10000 -h 127.0.0.1"));
    Demo.CartManagerPrx cartManager = Demo.CartManagerPrx.uncheckedCast(com.stringToProxy("CartManager:ssl -p 10000 -h 127.0.0.1"));

    try
    {
        String token = factory.createSecurityToken("foo", "dummy");
 
        cartManager.addItem("item1", 2, token);
        cartManager.removeItem("item1", token);
    }
    catch(PermissionDeniedException ex)
    {
       // Handle permission denied 
    }
    catch(com.zeroc.Ice.LocalException ex)
    {
       // Handle communication failure
    }

    com.destroy();
}

The client creates a proxy with the Ice object identity CartManager and the endpoint ssl -p 10000 -h 127.0.0.1. We assume here that the server is running on the same machine as the client; if the client and servers are on different hosts, we would use the IP address or hostname of the server's host with the -h option. We would also typically use a configuration property for this stringified proxy instead of hard-coding it the client's source code.

Improvements

Using Ice contexts

Explicitly passing the token with each call on the cart manager is cumbersome, and the security context is not something the remote APIs of the application should be concerned with. A better approach is to use requests context to send the security token with each invocation without an explicit token string parameter in each Slice operation signature:

Slice
interface CartManager
{
    void addItem(string itemId, int quantity) throws PermissionDeniedException;
    void removeItem(string itemId) throws PermissionDeniedException;
}

The client uses instead implicit contexts to pass this token:

Java
String token = cartManager.createSecurityToken("foo", "dummy");

// Setup an implicit context to provide the security token
java.util.Map<String, String> context = new java.util.HashMap<String, String>();
context.push("securityToken", token);
communicator.getImplicitContext().setContext(context);

cartManager.addItem("foo", "item1", 2);
cartManager.removeItem("foo", "item1");

The Ice communicator also needs to be configured to use the implicit context with the Ice.ImplicitContext property.

An alternative to implicit contexts is to set the context on the proxy:

Java
java.util.Map<String, String> context = new java.util.HashMap<String, String>();
context.push("securityToken", token);
cartManager = cartManager.ice_context(context);

Invocations made on this new cart manager proxy will now always embed the configured context. This solution works well if you only use few proxies.

On the server side, the cart manager servant can retrieve the context using the Ice current parameter:

Java
public void addItem(String userId, String itemId, int quantity, com.zeroc.Ice.Current current) throws PermissionDeniedException
{
    checkToken(current.ctx.get("securityToken")); // Ensures the given token is valid and generated for the given user
    ...
}

Using dispatch interceptors

An additional improvement we could make is to remove the security checks from the implementation of the cart manager interface. We can do this with an Ice dispatch interceptor to intercept requests on the cart manager Ice object. The security check on the token would no longer be done explicitly in the implementation of addItem and removeItem. Instead, the interceptor implementation would take care of it:

Java
class InterceptorImpl extends com.zeroc.Ice.DispatchInterceptor
{
    public InterceptorImpl(CartManagerImpl cartManager)
    {
        _cartManager = cartManager;
    }
 
    public java.util.concurrent.CompletionStage<com.zeroc.Ice.OutputStream> dispatch(Request request)
    {
        checkToken(request.getCurrent().ctx.get("securityToken"));
        return _cartManager.ice_dispatch(request);
    }
 
    private CartManagerImpl _cartManager;
}

Connection based security

Ice supports persistent connections with TCP based transports (tcp, ssl, ws, wss), so we can also keep track of authenticated clients through the connection associated with incoming requests. With this approach, the client authenticates with the server using credentials such as username/password. The server then checks these credentials and associates the current network connection with them. When the client sends a request to the server, the server can check the connection used for sending the request and see if the connection is known. Depending on the associated credentials it can reject or accept the request.

Let's update the Slice interfaces presented in the previous example to use connections instead of tokens:

Slice
exception PermissionDeniedException
{
    string reason;
}

interface CartManager
{ 
    void login(string userId, string password) throws PermissionDeniedException;
    void logout();

    void addItem(string itemId, int quantity) throws PermissionDeniedException;
    void removeItem(string itemId) throws PermissionDeniedException;
}

Server implementation

A sample Java implementation of the cart manager is shown below:

Java
class CartManagerImpl implements Demo.CartManager
{
    synchronized public void login(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkCredentials(userId, password);
        _connections.put(current.con, userId);
    }
 
    synchronized public void logout(com.zeroc.Ice.Current current)
    {
        _connections.remove(current.con);
    }
 
    synchronized public void addItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException
    {
        String userId = checkConnectionAndGetUserId(current.con); // Ensures the connection is authenticated
        java.util.Map<String, Integer> cart = _carts.get(userId);
        if(cart == null)
        {
            cart = new java.util.HashMap<String, Integer>()
            _carts.put(userId, cart);
        }
        cart.put(itemId, quantity);
    }
 
    synchronized public void removeItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException        
    {
        String userId = checkConnectionAndGetUserId(current.con); // Ensures the connection is authenticated
        java.util.Map<String, Integer> cart = _carts.get(userId);
        if(cart != null)
        {
            cart.remove(itemId);
        }
	}
 
    private String checkConnectionAndGetUserId(com.zeroc.Ice.Connection connection) throws PermissionDeniedException
    {
        String userId = _connections.get(connection);
        if(userId == null)
        {
            throw new PermissionDeniedException("unknown connection");
        }
        return userId;
    }
 
    // A map of userId -> Cart where Cart is  a map of item ID -> Quantity
    private java.util.Map<String, java.util.Map<String, Integer>> _carts;

    // A map of Ice.Connection to user ID
    private java.util.Map<com.zeroc.Ice.Connection, String> _connections;
}

As you can see, unlike the token based approach, the server is now required to keep client state with the new map of connections to user IDs. This map is populated when a client calls the login method. The implementation verifies the credentials by calling checkCredentials. We don't show the implementation of this method here but typically this method would perform a database lookup to check the password. Once the credentials are checked, the implementation adds the connection obtained from the Ice current object to a map and associates the user ID with the connection. The implementation of the logout method is simple: it just removes the map entry.

The addItem and removeItem methods retrieve the connection and obtain the user ID associated with the connection to retrieve the user's cart.

Client implementation

You will find below a sample client which uses the cart manager interface defined above:

Java
public static void main(String args[])
{
    com.zeroc.Ice.Communicator com = com.zeroc.Ice.Util.initialize(args);
    Demo.CartManagerPrx cartManager = Demo.CartManagerPrx.uncheckedCast(com.stringToProxy("CartManager:ssl -p 10000 -h 127.0.0.1"));
    try
    {
        cartManager.login("foo", "dummy");
        cartManager.addItem("item1", 2, token);
        cartManager.removeItem("item1", token);
        cartManager.logout();
    }
    catch(PermissionDeniedException ex)
    {
       // Handle permission denied 
    }
    catch(com.zeroc.Ice.LocalException ex)
    {
       // Handle communication failure
    }
    com.destroy();
}

With this approach, it is critical that the client's connection to the server remains open. If the connection is closed and a new connection is created, further requests from the client to the server will fail unless the client authenticates the new connection with a call to login.

This connection-based approach requires a good understanding of Ice connection management. For example, in the client above, the invocations on the cart manager proxy object will use the same underlying Ice connection because they are made on the same proxy, one after the other. If there is a 60 seconds delay between the addItem and removeItem calls, with Ice's default configuration, Ice active connection management will close the connection. The removeItem call will then fail because it gets called on a new un-authenticated connection.

Improvements

Using ACM heartbeats

Ice provides configuration properties to keep Ice connections open and alive, and avoid the automatic closure of idle connections.

For example, you can set the following properties for the client and the server:

# Client
Ice.ACM.Close=0      # CloseOff
Ice.ACM.Heartbeat=3  # HeartbeatAlways
Ice.ACM.Timeout=30
 
# Server
Ice.ACM.Close=4      # CloseOnIdleForceful
Ice.ACM.Heartbeat=0  # HeartbeatOff
Ice.ACM.Timeout=30

With this configuration:

  • the client will never close a connection when it's idle, instead it will send a heartbeat message every 15 seconds to keep the connection alive,
  • the server will forcefully close a connection if it doesn't receive activity on the connection within the 30s period.

For more information on these configuration properties, see Ice activation connection management.

Using connection callbacks 

Ice also provides a callback mechanism to notify applications of connection closure. Such a close notification is useful to prevent leaks. In the server implementation shown above, if the client doesn't call logout (because it crashes or the network connection is dropped), the entry for the client's connection won't be removed from the _connections map. This creates a leak in the server.

To prevent such a leak, we can modify the login method  to ensure the entry is removed when the connection is closed:

Java
class CartManagerImpl implements Demo.CartManager
{
    synchronized public void login(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkCredentials(userId, password);
        _connections.put(current.con, userId);
        current.con.setCloseCallback(_callback);
    }
 
    ...

    private com.zeroc.Ice.ConnectionCallback _callback = new com.zeroc.Ice.CloseCallback()
    {
        public void closed(com.zeroc.Ice.Connection con)
        {
            synchronized(CartManagerImpl.this)
            {
                _connections.remove(con);
            }
        }
    }
}

Session based security

With this pattern, the client establishes a session with the server. The authentication information is sent by the client on session establishment. If the authentication succeeds, the server creates a session and returns a session identifier to the client. The client then uses the session identifier to perform further requests on the server application. The server persists the session information either in memory or in a database. In addition to the user credentials, the session stores extra information specific to the application. For example, in a shopping cart type application, the session can store the user's cart.

Here’s how we could write the Slice interfaces for such an application:

Slice
module Demo
{

interface Session
{
    void addItem(string itemId, int quantity);
    void removeItem(string itemId, int quantity);
    void destroy();
}

interface SessionFactory
{
     Session* createSession(string username, string password);
}

}

The object-oriented nature of Ice makes it easy to understand the interactions between the client and server. As you can see, there's also no added clutter to transmit the session identifier. The session identifier is implicitly encapsulated with the session proxy returned by the createSession call.

Let's see how these interfaces can be implemented and used.

Server implementation

Here's a sample implementation of the session interface:

Java
class SessionImpl implements Demo.Session
{
    public SessionImpl(String userId)
    {
       _userId = userId;
    }

    synchronized public void addItem(String itemId, int quantity, com.zeroc.Ice.Current current)
    {
        _cart.add(itemId, quantity);
    }
 
    synchronized public void removeItem(String itemId, com.zeroc.Ice.Current current)
    {
        _cart.remove(itemId);
    }

    public void destroy(com.zeroc.Ice.Current current)
    {
        current.adapter.remove(current.id);
    }
 
    private String _userId;
    private java.util.Map<String, Integer> _cart = new java.util.Map<String, Integer>();
}

The session information is stored in-memory by instances of the SessionImpl object. There's one instance of this object per client session. The addItem and removeItem methods are called by Ice clients to add or remove items from the cart. The destroy method is called by the client to notify the server it's no longer interested in the session. The implementation of this method un-registers the Ice object from the Ice object adapter; thereafter, Ice no longer accepts requests to this session object.

The current.id value is the Ice object identity of the Ice object. Let's see how the session Ice objects are created to better understand what this identity is:

Java
class SessionFactoryImpl implements Demo.SessionFactory
{
    public Demo.SessionPrx createSession(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkUserNameAndPassword(userId, password); // Perform authentication check on username/password
        return Demo.SessionPrx.uncheckedCast(current.adapter.addWithUUID(new SessionImpl(userId)));
    }
}

The implementation of the createSession methods checks the credentials supplied by the client. In this example, the client provides a user identifier and password. Again, it could also provide another type of credential, such as a X.509 certificate.

This method then creates a new SessionImpl servant object, and registers this servant with the Ice object adapter, using a UUID for the object identity. A proxy to this Ice object is then transmitted back to the client. This proxy embeds the Ice object identity and the endpoint information to allow the client to reach this Ice session object remotely.

This proxy is in effect the session identifier that a traditional web application would transmit back to a client. The random nature of the UUID used for the Ice object identity ensures that other client can't easily guess it and with a secured communication between the client and server, the proxy identity can't be discovered by eavesdropping on the network connection.

The main method of the server is similar to the one we used for the token based model:

Java
public static void main(String args[])
{
    com.zeroc.Ice.Communicator communicator = com.zeroc.Ice.Util.initialize(args);
    com.zeroc.Ice.ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints("SessionAdapter", "ssl -p 10000");
    adapter.add(new SessionFactoryImpl(), new com.zeroc.Ice.Identity("SessionFactory", ""));
    communicator.waitForShutdown();
}


Client implementation

Below is a simple client that creates a session and update the session's cart.

Java
public static void main(String args[])
{
    com.zeroc.Ice.Communicator communicator = com.zeroc.Ice.Util.initialize(args);
    com.zeroc.Ice.ObjectPrx proxy = communicator.stringToProxy("SessionFactory:ssl -p 10000 -h 127.0.0.1");
    Demo.SessionFactoryPrx factory = Demo.SessionFactoryPrx.uncheckedCast(proxy);
 
    try
    {
        Demo.SessionPrx session = factory.createSession("foo", "dummy");
        session.addItem("item1", 2);
        session.removeItem("item1");
		session.destroy();
    }
    catch(com.zeroc.Ice.LocalException ex)
    {
       // Handle communication failure
    }
 
    communicator.destroy();
}

The client creates a proxy for the session factory Ice object and invokes createSession to provide the credentials and create the session. The client can then use the session proxy to add and remove items from the cart. Finally, it calls destroy on the session to ensure the resources allocated by the server for the session are destroyed.

Improvements

Session lifecycle

In the server implementation above, we rely on the client to call the session destroy() method for releasing the resource allocated for the session. The implementation removes the servant from the Ice object adapter. If destroy() isn't called, the servant will remain referenced by the Ice object adapter and this will cause the object to remain in memory until the Ice object adapter is destroyed when the server is shutdown.

Since there's no guarantee that the client will call destroy() (the client could crash, the network connection could be lost, ...), we need to add a mechanism to remove the session when we detect that the client abandoned the session without calling destroy(). How can the Ice server detect this? There are several solutions:

  • the server requires the client to explicitly "ping" or keep-alive the session at regular intervals. The server keeps track of the pings from the clients and reaps sessions for which it didn't receive a ping message for a while. This can be implemented by setting a timer in the client to call the ice_ping() method on the proxy and override the ice_ping servant method in the Demo.SessionImpl class. The server checks at regular intervals which sessions can be reaped based on the last time the ping was received.
  • the application can bind the session to the Ice connection (which represents the persistent network connection between the client and server) and rely on the connection closure callback to destroy the session. This approach requires that the client uses a single connection to invoke on the server. This is true unless the client explicitly requests the use of separate connections, or uses different connection timeouts or endpoints.

Binding the session to the client connection is the simplest solution. Here's how we can modify the session factory to setup a connection callback to cleanup the session when the connection is closed:

Java
class SessionFactoryImpl implements Demo.SessionFactory
{
    public Demo.SessionPrx createSession(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkUserNameAndPassword(userId, password); // Perform authentication check on username/password
 
        Demo.SessionPrx sessionPrx = Demo.SessionPrx.uncheckedCast(current.adapter.addWithUUID(new SessionImpl(userId)));
        current.con.setCloseCallback(new com.zeroc.Ice.CloseCallback()
        {
            public void closed(com.zeroc.Ice.Connection con)
            {
                current.adapter.remove(sessionPrx.ice_getIdentity());
            }
        });
        return sessionPrx;
    }
}

There is one additional consideration here: how quickly the closed callback is called. In the event the operating system's TCP/IP stack doesn't detect a connection failure in a timely manner, it's possible for the callback closed method to be called several hours after the connection failure. This is typically the case when the client connection to the server goes through several routers and the connection failed because of an expected failure on the connection path. In order for the server to release the session resource in a timely manner and quickly detect the connection loss, you can enable keep-alives on the connection. The Ice Manual provides more information on this topic in the active connection management section.

Increased security

The approach above relies on the fact that other clients can't discover the UUID used for the Ice object identity of the session. In the improvement above, we saw that binding the session to the connection was a simple way to cleanup the session resources when the client goes away. To provide increased security, we can also rely on the Ice connection object to check that invocations on the session are only performed by the client that created the session. This requires to keep track of the Ice connection objects in the session and check this connection in each call:

Java
class SessionImpl implements Demo.Session
{
    public SessionImpl(String userId, com.zeroc.Ice.Connection connection)
    {
       _userId = userId;
       _connection = connection;
    }
    synchronized public void addItem(String itemId, int quantity, com.zeroc.Ice.Current current)
    {
        checkConnection(current);
        _cart.add(itemId, quantity);
    }
 
    synchronized public void removeItem(String itemId, com.zeroc.Ice.Current current)
    {
        checkConnection(current);
        _cart.remove(itemId);
    }

    void destroy(com.zeroc.Ice.Current current)
    {
        checkConnection(current);
        current.adapter.remove(current.id);
    }
 
    private void checkConnection(com.zeroc.Ice.Current current)
    {
        //
        // If the connection doesn't match the one used to create the session, we return
        // ObjectNotExistException to the client.
        //
        if(current.con != _connection)
        {
            throw new com.zeroc.Ice.ObjectNotExistException(current.id, current.facet, current.operation);
        }
   }
 
    private String _userId;
    private com.zeroc.Ice.Connection _connection;
    private java.util.Map<String, Integer> _cart = new java.util.Map<String, Integer>();
}

Using dispatch interceptors

Just like for the token-based model, we can further improve the session implementation shown above and move the checkConnection call to a dispatch interceptor:

Java
class InterceptorImpl extends com.zeroc.Ice.DispatchInterceptor
{
    InterceptorImpl(SessionImpl session)
    {
        _session = session;
    }

    public java.util.concurrent.CompletionStage<com.zeroc.Ice.OutputStream> dispatch(com.zeroc.Ice.Request request)
    {
        _session.checkConnection(current);
        return _session.ice_dispatch(request);
    }
 
    private SessionImpl _session;
}