Saturday 5 November 2011

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());
}

No comments:

Post a Comment