Hoy voy a volver atrás en el tiempo y escribir un articulo acerca de la tecnología implementada en el Proyecto de Fin de Master que ya he comentado en otros artículos. En este proyecto se implementó una capa de seguridad para la comunicación móvil con la api rest implementada en Symfony2. Vamos a conocer los detalles de la implementación
¿Que es Web Service Security?
WSSE o Web Service Security es una especificación de seguridad definida por SOAP, se le conoce también como WS-Security y permite proteger los mensajes entrantes y salientes contra distintas amenazas de seguridad.
Al solicitar un sistema de autenticación basado en tokens, se puede limitar el acceso a organizaciones o individuos no autorizados a estos métodos. Además agregan una marca de tiempo para evitar que los mensajes se reproduzcan repetidamente.
WSSE tiene muchas ventajas en cuestiones de seguridad, sin embargo desde el punto de vista de desarrollador, lo que hemos implementado es como consumir servicios web enviando tokens de autenticación en los encabezados del WSSE.
El problema principal es que no podemos exponer una api totalmente pública, aunque algunos métodos puedan estar abiertos a cualquier cliente hay datos de usuarios que solo deben proveerse al usuario adecuado, por ello debe de proporcionarse un sistema de autenticación. Por ejemplo los métodos que proporcionan juegos recomendados en función del cliente autenticado debe estar protegido por el usuario autenticado.
Por ello decidimos implementar un proveedor de autenticación que proteja estos métodos. Por ejempl sio queremos recibir los datos de los partidos de un usuario concreto, pero que no puedan acceder a dicha información usuarios que no deberían.
Lo primero de todo es que debemos exponer los salt de los usuarios para implementar este sistema de seguridad. El salt es empleado para mejorar la seguridad de la aplicación, este salt se emplea junto al password para generar el resumen que se verifica para proveer la autenticación en la aplicación y es único por cada usuario. Sin embargo requerimos este salt para generar los resúmenes en las peticiones de WSSE. Compartir este salt no vulnera nuestra aplicación ya que tenemos un único salt por usuario ya no se podría decodificar todas las password de nuestra aplicación a través de tablas hash.
Implementar este protocolo provee varios beneficios:
- Encriptación de la password
- Seguro frente ataques repetitivos usando cache de tokens
- Sencilla configuración
Esta implementación es muy útil para securizar nuestra API. Las bases de WSSE proporcionan una cabecera en las peticiones enviadas a la api mandando unas credenciales encriptadas, una verificación de tiempos , un token único por petición denominados “nonce” y un resumen de la password guardada en base de datos.
De esta manera, por ejemplo haríamos nuestra primera petición a la api con la siguiente estructura en la cabecera X-WSSE de la petición HTTP:
1 |
UsernameToken Username="alonsus91",PasswordDigest="LlsDqVDaw5nMs1iasbladXWvs5c=",Nonce="YzdlMzQ3NWQ4MTc1YTI3OA==", Created="2014-05-30T07:53:54Z" |
Generacion de tokens en aplicacion android
El resumen de la password se genera concatenando el token autogenerado en base 64, la fecha de creación y la password codificada. A este cadena se le aplica el algoritmo hash sha1 y se codifica de nuevo en base 64.
Es decir la parte cliente debería de realizar el siguiente procedimiento al intentar autenticarse para emplear la API. Primero necesitamos un generador de tokens “nonce”. Apoyándonos en el componente de seguridad de java podemos obtener estos tokens
1 2 3 4 5 |
private String generateNonce() { SecureRandom random = new SecureRandom(); byte seed[] = random.generateSeed(10); return bytesToHex(seed); } |
Además de generar el token nonce requerimos generar una fecha válida que enviaremos en la petición:
1 2 3 4 |
private String generateTimestamp() { sdf.setTimeZone(TimeZone.getTimeZone("UTC")); return sdf.format(new Date()); } |
Una vez tenemos el token y el timestamp solo debemos de generar el resumen que se enviará y se comprobará en la parte servidor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private String generateDigest() { String digest = null; try { MessageDigest md = MessageDigest.getInstance("SHA-1"); StringBuilder sb = new StringBuilder(); sb.append(this.nonce); sb.append(this.createdAt); sb.append(this.user.getPassword()); byte sha[] = md.digest(sb.toString().getBytes()); digest = Base64.encodeToString(sha,Base64.NO_WRAP); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return digest; } |
Tras obtener este resumen podemos generar la cabecera necesaria para enviarla en nuestras peticiones HTTP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public String getWsseHeader() { StringBuilder header = new StringBuilder(); header.append("UsernameToken Username=\""); header.append(this.user.getUsername()); header.append("\", PasswordDigest=\""); header.append(this.digest); header.append("\", Nonce=\""); header.append(Base64.encodeToString(this.nonce.getBytes(), Base64.NO_WRAP)); header.append("\", Created=\""); header.append(this.createdAt); header.append("\""); return header.toString(); } |
Recepción de tokens en el lado servidor
En la parte servidor recibiremos esta cabecera y debemos de actuar coherentemente a la especificación. Para ello hemos implementado en PHP un proveedor de Autenticación que valide estas peticiones. Lo primero de todo necesitamos un Listener que capture todas las peticiones que se realizan sobre la estructura de urls api.
Este listener lee de la request la cabecera X-WSEE y valida que tenga el formato adecuado
1 2 3 4 5 |
$wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/'; if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) { return; } |
Tras parsear estos valores se los enviamos al proveedor de autenticación
1 2 3 4 5 6 |
$token = new WSSEUserToken(); $token->setUser($matches[1]); $token->digest = $matches[2]; $token->nonce = $matches[3]; $token->created = $matches[4]; $returnValue = $this->authenticationManager->authenticate($token); |
Este proveedor de autenticación trae el usuario de base de datos y comprueba si el usuario existe y en ese caso si los datos enviados en la petición son validos con los almacenados en base de datos. En caso afirmativo, se genera un token de autenticación que se guardará en la sesión, permitiendo hacer repetidas peticiones con el usuario sin tener que volver a realizar el proceso. También se podría haber implementado stateless y en cada petición realizar este proceso, pero además de ser algo pesado, preferimos hacerlo de esta manera y establecer un tiempo máximo de sesión
1 2 3 4 5 6 7 |
$user = $this->userProvider->loadUserByUsername($token->getUsername()); if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) { $authenticatedToken = new WSSEUserToken($user->getRoles()); $authenticatedToken->setUser($user); return $authenticatedToken; } |
La comprobación del resumen es bastante sencilla. Se comprueba que los timestamp mandados no sean del futuro ni hayan pasado 5 minutos de la petición, además se cachean los token “nonce” enviados para evitar ataques repetitivos. Finalmente se realiza el proceso realizado en cliente obteniendo el resumen de la password y comprobando si el valor es idéntico al enviado en la petición:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
if (strtotime($created) > time()) { return false; } if (time() - strtotime($created) > 300) { return false; } if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) { throw new NonceExpiredException('Previously used nonce detected'); } if (!is_dir($this->cacheDir)) { mkdir($this->cacheDir, 0777, true); } file_put_contents($this->cacheDir.'/'.$nonce, time()); // Validate Secret $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true)); return $digest === $expected; |
Para el desarrollo de esta comunicación basada en tokens me apoyé en el siguiente simulador: http://www.teria.com/~koseki/tools/wssegen/
Para finalizar, existen mas detalles de implementación en los repositorios. Como hemos visto es importante esta securización y la elegida es una opción entre otras, aunque ahora mismo se esta extendiendo el empleo del protocolo OAuth para estas comunicaciones.