Saturday 5 November 2011

Authentication and Authorization

When securing your web service there are two main things you need to worry about. Authentication and Authorization. 

I am sure everyone is familiar with authentication, since every time we log into Facebook  or our banks website we need to enter our user name and passwords. This is then verified against the user name and password set when you created your account for authentication. 

Once you are authenticated you are restricted to only viewing and editing content that you are authorized to view. In Facebook, you are not authorized to edit strangers profiles and you are not authorized to access the administrators endpoints.  

For each endpoint you visit on the web service you need to be both authorized and authenticated. Typically with web pages you would use http authentication schemes such as basic access authentication or  digest access authentication. Then depending on how secure your service needs to be, you can use TSL  to encrypt all communication. Once a user is authenticated they stay logged in for the session and don't need to authenticate again. However, one of the principles of REST is that it is stateless, so we shouldn't be saving session data between calls to the server. For each call to an endpoint we should be sending enough data to the sever to authenticate the user. 

For this project I decided to use 2 legged oAuth, since the client will be written in actionscript and not an http web browser. Well truth be told, there is no actual 2 legged oAuth 1.0. So I am using a cut down version, where we use the consumer key and consumer secret to authenticate the client and use the oAuth signature methods to validate them. Since REST is stateless we need to send enough information to authenticate the user with each call to the endpoint. We could send an oAuth signature with each call but instead I:
-  Have the user log in which uses oAuth to authenticate the users.
- We then create a session key to save into our DB and return it to the user.
- Each subsequent call I include the session key with the call and use that to identify the user. 

Unless you secure the connection with TLS there are some obvious security issues,  such as someone packet sniffing can getting your session key. Or someone could guess your session key, so we make sure that they expire within a short time of not hearing from the users. The more I think about it the less I like creating a session key and the more I like the idea of using oAuth to authenticate each call. It would be more secure since we don't have a session key to hijack.

Where authentication is checked once up front, authorization is something that has to be integrated all over your code. First off you can restrict the access to endpoints by using the jersey annotation @RolesAllowed, such as:
       @PUT
       @Consumes(MediaType.APPLICATION_XML)
       @RolesAllowed({Role.ADMIN_STRING, Role.CS_STRING, Role.USER_STRING})
       public DetailedCustomerTrans updateCustomer(DetailedCustomerTrans custTrans)

But that doesn't help when you have an endpoint that allows a user to update their details. All customers have access to that endpoint, but you don't want to authorize Customer A to edit Customer B's data. So you need to specifically code those business rules into your program.

Authentication and Authorization code examples

Now that we have the basics covered let's get into the code.
All the code below can be found in fullwith my open source project:
In the flex client we use a library found at http://code.google.com/p/oauth-as3/ to build the oAuth signature.
var oAuthConsumer:OAuthConsumer =
       new AuthConsumer(customer.personaName, customer.password);
var signMethod:IOAuthSignatureMethod =
       new OAuthSignatureMethod_HMAC_SHA1();
var oAuthRequest:OAuthRequest =
       new OAuthRequest(OAuthRequest.HTTP_MEHTOD_POST,
       ServerDAODefines.serverURL + mSessionPath, null, oAuthConsumer);
                    
var urlString:String = oAuthRequest.buildRequest(signMethod);
var uri:URI = new URI(urlString);

client.post(uri, new ByteArray());

We use this to call the endpoint to create a session. However, since we are not authenticated at this point we need to make sure the endpoint is open to all. We can do that using the Jersey annotation @PermitAll. This will allow all users access to this endpoint.
@POST
@Produces(MediaType.APPLICATION_XML)
@PermitAll
public SessionResultTrans createSession(@Context HttpContext hc) {
Now we use the Jersey oAuth libraries to read in the oAuth signature to verify the users.
try {
    // wrap incoming request for OAuth signature verification
    OAuthServerRequest request = new OAuthServerRequest(hc.getRequest());

    // get incoming OAuth parameters
    OAuthParameters params = new OAuthParameters();
    params.readRequest(request);
    String consumerKey = params.getConsumerKey();
    OAuthSecrets secrets = new OAuthSecrets();
                   
    //get the customer associated with the session.
    CustomerFacade custFacade = new CustomerFacade();
    CustomerBO cust = custFacade.getCustomerByPersona(consumerKey);
    if(cust == null) {
        throw new ApplicationException(Status.UNAUTHORIZED,
                ReturnCodes.UNKNOWN_ERROR.toString(), "unable to find Customer "
                + consumerKey);
    }
                       
    //... set secrets based on consumer key and/or token in parameters ...
    secrets.setConsumerSecret(cust.getPassword());
           
   //Authenticate the user using OAuth
   if(!OAuthSignature.verify(request, params, secrets)) {      
       throw new ApplicationException(Status.UNAUTHORIZED,
                        ReturnCodes.UNKNOWN_ERROR.toString(), "failed to
                        verify  OAuthSignature " + cust.getCustomerId());
}


The session key is a randomly generated string that we save into the Db and return to the client. Below is the code we use to create the session key.

public final class SessionKey {
       
        static final String symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        static final int numSymbols = symbols.length();
        static Random rnd = new Random();

        /**
         * The constructor is private since this is a utility class
         * that does not need to be instantiated
         */
        private SessionKey() {
               
        }
       
        /**
         *  generates random XX character long session keys based on the
         *  symbols String
         * @param len number of characters in the generated session key
         * @return session key
         */
        public static String createSessionKey(int len) {
                StringBuilder key = new StringBuilder();
                for(int i = 0; i < len; i++) {
                        key.append(symbols.charAt( rnd.nextInt(numSymbols) ));
                }
                return key.toString();

        }
}

The session in the DB saves the unique session Id, the customer associated with the session and some timestamps to validate the session against.

PreparedStatement preparedStatement = null;
               
try {
        String sqlQuery = "INSERT INTO session VALUES(?, ?, "
                + "UTC_TIMESTAMP(), UTC_TIMESTAMP())";
        preparedStatement = connect.prepareStatement(sqlQuery);
        preparedStatement.setString(1, sessionBO.getSessionId());
        preparedStatement.setInt(2, sessionBO.getCustomerId());
                       
        int rowUpdated = preparedStatement.executeUpdate();
        if(rowUpdated == 0) {
                throw new ApplicationException(ReturnCodes.UNKNOWN_ERROR.toString(),
                         "failed to create session with " + sessionBO.getCustomerId() +
                         " no rows updated");
        }
                       
} catch (SQLException e) {
        throw new ApplicationException(e, ReturnCodes.UNKNOWN_ERROR.toString(),
                 "failed to create session with " + sessionBO.getCustomerId());
} finally {
        try { if(preparedStatement != null) preparedStatement.close(); }
        catch (Throwable ignore) { /* Propagate the original exception */ }                    
}

For the rest of the interactions between the client and server, we send the session key with each request in the header.

var uri:URI = new URI(ServerDAODefines.serverURL + 
            mCustomerPath + "/" + mFindByPersonaName +
            "/" + persona);
var request:HttpRequest = new Get();
request.contentType = "text/plain";
request.addHeader("sessionid", sessionKey);
request.addHeader("Accept", "application/xml");

client.request(uri, request);

On the servers side we don't want authenticate the users in each endpoint, so we setup a filter to authenticate the users. To setup this up we need to add some initialization parameters to jersey to tell it about our filter. The first filter is our filter we use to authenticate the user and the second filter enforces the authorization for the roles allowed to access each endpoint

    <init-param>
        <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
        <param-value>com.cred.industries.platform.filter.AuthenticationFilter</param-value>
    </init-param>
    <init-param>
         <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
         <param-value>com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory</param-value>
    </init-param>

In the filter we get the session Id out of the header and use it to get the session from the DB.  With the session data we can figure out what customer is associated with the session.
@Override
public ContainerRequest filter(ContainerRequest cr) {
               
        String sessionId = cr.getHeaderValue(SESSION_PARAMETER);
                       
        SessionFacade session = new SessionFacade();
        CustomerBO cust = session.authenticateUserbySessionId(sessionId);


We also save the customer into the thread local storage, so anywhere in this thread we can find out who is the requesting customer. This is used later on to see if the user is authorized.
With the customer we setup the security context so jersey can filter the requests authorization at each endpoint. Jersey needs a Security context Object to authorize the roles against.

        cr.setSecurityContext(new Authorizer(cust));
        return cr;
}

/**
* <p>SecurityContext used to perform authorization checks.</p>
*/
public class Authorizer implements SecurityContext {

        private CustomerBO mCustomer;
       public Authorizer(final CustomerBO customer) {
                mCustomer = customer;
        }

        public Principal getUserPrincipal() {
                return new Principal() {
                        public String getName() {
                        return mCustomer.getRoles().getHighestRole().toString();
                }
        };
}

        public boolean isUserInRole(String role) {
               
                return mCustomer.getRoles().hasRole(Role.convert(role));
        }

        public boolean isSecure() {
                return "https".equals(mUriInfo.getRequestUri().getScheme());
        }

        public String getAuthenticationScheme() {
                return SecurityContext.BASIC_AUTH;
        }
}


That covers authentication and the authorization of endpoints, but for authorization of business objects I created an interface that each of my business objects inherits. With this we can easily query the business objects to see if we are authorized to access or modify.

public interface Authorization {

        /**
         * used to control who can update this BO.
         * @return true if the authenticated can modify the BusinessObject
         */
        boolean authorizedToModify();
       
        /**
         * used to control who can read this BO.
         * @return true if the authenticated can read the BusinessObject
         */
        boolean authorizedToAccess();

        /**
         * used to control access to parts of the BO. For example, in the
         * Customer BO we only want a           
         * super user to be able to change the persona name.
         * @return true if the authenticated user can modify/read any
         * part of the BusinessObject as the SuperUser
         */
        boolean authorizedAsSuperuser();

}

Here is an example of how my customer business object implements this interface
                       
/**
 * returns if the currently authenticated customer is authorized to modify
 * this customer BO.
 * a CS, admin or yourself can make changes to this BO
 * @return true if this is a CS, admin or yourself
 */
@Override
public boolean authorizedToModify() {
                       
                        //a CS, admin or yourself can make changes to this BO
                       
                        //thread local storage we setup with customer at start to access
                        //anywhere with this request. Only lives as long as the request.
                        CustomerBO authenticatedUser = SessionData.getSessionCustomer();      
                       
                        CustomerRolesBO roles = authenticatedUser.getRoles();
                        return authenticatedUser != null
                                                && (roles.hasRole(Role.ADMIN)
                                                || roles.hasRole(Role.CS)
                                                || mCustomerId == authenticatedUser.getCustomerId());
}

/**
 * anyone can access this BO
 * @return true
 */
@Override
public boolean authorizedToAccess() {
                                               
                        //any one can read this BO
                        return true;
}
                       
/**
 * returns if the currently authenticated customer is a super user
 * Ie a user with permissions to change anything related to this BO
 * In this case a CS or admin
 * @return true if this is a CS, or admin.
 */
@Override
public boolean authorizedAsSuperuser() {
                       
                        //a CS, admin or yourself can make changes to this BO
                        CustomerBO authenticatedUser = SessionData.getSessionCustomer();
                        CustomerRolesBO roles = authenticatedUser.getRoles();
                        return authenticatedUser != null
                                                && (roles.hasRole(Role.ADMIN)
                                                ||  roles.hasRole(Role.CS));
}

Then in code when using the customer business object we can quickly access the authorizations to prevent or allow access

boolean isAuthorizedToModify = custToModify.authorizedToModify();
               
//you are only allowed to modify your account unless you
//are an admin or CS
if(!isAuthorizedToModify) {
        throw new ApplicationException(Status.UNAUTHORIZED,
        ReturnCodes.UNKNOWN_ERROR.toString(), "not authorized to modify " +
        custToUpdate.getPersonaName() + " by " + authenticatedUser.getPersonaName());
}
               
//only a super user for the BO can change the persona name
if(isSuperUser) {
        custToModify.setPersonaName(custToUpdate.getPersonaName());
}