September 2nd, 2009 Category: SOA Zend Framework
6 Comments »
En el proyecto actual en el que estoy trabajando nos topamos con un problema para validar que un usuario solo pueda ver sus datos y/o productos y no los de otro usuario. Este problema se da porque el proyecto actual esta muy orientado a SOA, servicios orientado a la arquitectura, básicamente esto significa subdividir una aplicacion en aplicaciones mas chicas, generalmente están desacopladas unas de otras, y cada una funciona como un ser independiente. La comunicacion entre ellas es a través de los conocidos Web Services (rest, xml-rpc, soap, json ).
Una de estas aplicaciones es un Single Sign-on el cual administra todo lo referente a los usuarios, sesiones, datos, etc. Otra de las aplicaciones es un Shopping cart, este tiene las ordenes asociadas a un id de usuario, como conte antes la idea es que estén lo mas desacopladas posibles, esto es para poder reutilizar estas apis en otros proyectos evitando reescribir código, y con tocar algunos items de configuracion salga andando.
Si desde una tercera aplicacion quiero traer los items agregados por un usuario, debería pedirle al shopping cart los items a partir del id de usuario. Por ejemplo, si la api de shopping cart tiene la siguiente interfaz.
public getOrder( $userId );
public getItems( $orderId );
public payOrder( $orderId, $method );
Si nosotros queremos traer la orden abierta de un usuario enviamos el id del usuario y obtenemos el id de la orden, con el mismo conseguimos los items, y porque no pagarlo. Para evitar este problema, tendríamos que validar que el que esta ejecutando estas funciones este logueado en el sistema, y no alguien aprobechandose de la falta de validacion.
La forma logica de hacer esto es validar la sesión de usuario, pero como comente antes esta opción no es posible porque las aplicaciones están separadas, pueden llegar a estar en el mismo server como puede que no.
Para solucionar esto utilizamos un sistema parecido al de Kerberos, aprovechando que tenemos un sistema donde centralizamos los usuarios ( el Single Sign-on), vamos a usarlo para emitir certificados que se van a poder utilizar para validar los datos de un usuario.
Este certificado como dijimos lo emite el single sign-on, el mismo va a viajar encriptado, cada aplicacion va a tener conocimiento de como desencriptarla.
Cada vez que nosotros busquemos los items del carrito de compras de un usuario, vamos a hacer la llamada pasando el certificado, el id de usuario, y los parámetros específicos que requiera cada método de la api.
Cuando se desencripta el certificado con una cable en la configuracion de cada api obtenemos un objeto PHP el cual tiene 3 propiedades, userId, timestamp, y un token. El userId sirve para validar que el id de usuario que nos envían es el mismo que el certificado, el timestamp es para saber si el certificado sigue vigente o caduco, el token, es solo para que el certificado nunca sea el mismo, y sea difícil interceptarlo.
Aplicando esta solución, la interface del Shopping cart nos quedo así
public getOrder( $userId, $credential );
public getItems( $orderId, $userId, $credential );
public payOrder( $orderId, $method, $userId, $credential );
Hay muchos protocolos de encriptacion el tema es bastante amplio y hay para todos los gustos. En este caso nosotros nos decidimos por Mrcrypt, esta era la opcion mas comoda, no solo por que viene por default en PHP, sino porque tambien hay un componente de Zend Framework que nos deja aprovechar esta extension.
Esta situacion se pueda dar en varias situaciones, no precisamente en este aplicacion en particular, tambien se puede aplicar el sistema de clave publica y privada pero hay una funcion en php para usar GnuPG (http://ar.php.net/manual/en/function.gnupg-encrypt.php), no lo probe ni investigue mucho, pero si esto no soluciona el problema podemos usarlo desde command line, en Devzone.zend.com encontre este articulo que nos puede ayudar un poco mas http://devzone.zend.com/article/1265
Para implemntar esta solucion en la aplicacion en la que trabajo use Zend_Filter, basicamente Zend_Filter toma un valor le aplica ciertas reglas, y devuelve un resultado filtrado por estas reglas, por ejemplo un nombre de usuario tiene que estar en minuscula, sin espacios, sin guiones medios ( – ), para esto nosotros vamos a aplicarle una funcion para que le limpie los espacios en los extremos, le quite los guiones, o caracteres que no queramos, y ejecute un strtolower para devolver el string en minuscula. Todo esto aplica como filtro, a un valor se le puede aplicar mas de un filtro. Zend_Filter nos da un conjunto de Filtros mas complejos que un simple trim(), o strtolower.
Zend_Filter nos provee dos filtros que en realidad son uno solo (para saber el porque mirar el codigo fuente de Zend_Filter_Decrypt ), Zend_Filter_Decrypt y Zend_Filter_Encrypt.
Estos nos sirven para recibir un parametro encriptarlo, y desencriptarlo.
Es muy sencillo utilizarlo pero tiene algunas cosas que por ahi no quedan tan claro. Para este caso yo use mrcrypt y aproveche el adapter de Mrcrypt que esta para este filtro. Al usar un adapter obtenemos un monton de propiedades que en el caso de no agregar el adapter tendriamos que completarlas nosotros mismos una por una. El problema con el que nos encontramos es que si o si necesitamos pasar el adapter, y el vector, el vector basicamente es la cantidad de bytes que va a tener cada bloque de encriptacion (para mas leer la wiki), este parametro es obligatorio, porque sino lo ingresamos usa un valor random, y seria imposible desencriptarlo sin la cantidad exacta de bytes por bloque. No entendi porque la gente de Zend no incluyo un valor default para los vectores pero alguna razon tendran.
En el ejemplo que vamos a ver a continuacion, se crea un objeto con ciertas propiedades, lo serializamos, lo encriptamos, lo pasamos a base64, para que podamos enviarlo via get a otros sistemas.
$obj = new stdClass();
$obj->userId = 12;
$obj->timestamp = time();
$obj->token = sha1(uniqueid(mt_rand(),true);
$objSerialized = serialize( $obj );
$encrypt = new Zend_Filter_Encrypt(
array(
'adapter' => 'mrcrypt'
, 'vector' => '12345678'
)
);
//Aplicamos el filtro a nuestro objeto serializado, esto nos devuelve un binario encryptado
$objEncrypted = $encrypt->filter( $objSerialized );
//Convertimos el binario en algo que podamos usar mas facilmente
$credential = base64_encode( $objEncrypted );
?>
El valor que obtenemos de esta operacion va a ser nuestro certificado.
Cuando queramos desencriptarlo el codigo que tenemos que aplicar a nuestro certifiado es el siguiente
$encrypt = new Zend_Filter_Decrypt(
array(
'adapter' => 'mrcrypt'
, 'vector' => '12345678'
)
);
$objEncrypted = base64_decode( $credential);
$objSerialized = $encrypt->filter( $objEncrypted );
$obj = unserialize( $objSerialized );
/* ahora mostramos nuestro objeto desencriptado y desserializado */
Zend_Debug::dump( $obj );
Ademas de obtener el objeto deseariliazado ahora nos toca validar la credencial, para esto con el objeto deserealizado comparamos si el userId que nos llega es el mismo que en la credencial, y si la fecha es cercana al timestamp que tiene la credencial. Como parametro exta mis credenciales tienen un timestamp y timelife, que me dice cuanto puede vivir un certificado.
A Zend_Filter_Decrypt y Zend_Filter_Encrypt podemos agregarle un salt que va a darle mas seguridad a nuestros certificados.


