Autenticación LDAP en Symfony

Buenas, hoy voy a hablaros de autenticación de usuarios usando LDAP en Symfony.

Symfony es un framework PHP, el cual ofrece la posibilidad de usar LDAP para autenticar usuarios. Pero sólo eso, la “posibilidad” nada más (que no es poco). Nosotros tenemos que crear la clase que conecte symfony con el servidor ldap y que modifique, cree y haga todo lo necesario en el mismo. Exactamente eso es lo que tendremos al terminar de leer esta entrada.

¿Por qué usar LDAP?

De esta manera usamos un servicio de autenticación genérico. Imagina que nosotros tenemos nuestra página web con sus usuarios y de repente se nos antoja instalar un foro o un blog o un servidor xmpp… software de terceros que manejan los usuarios como quieren. Con LDAP nos olvidamos de todos los rollos, configuramos todo ese software para que use LDAP y listo.

¿Cómo trabaja Symfony con LDAP?

Es simple. Symfony sigue manteniendo su base de datos pero ademas comprueba que el usuario es válido en el servidor LDAP. Esto es que cuando te autenticas en Symfony, primero mira si el usuario es válido en la base de datos de Symfony y si es válido lo mira en LDAP. Si uno de los dos falla, la autenticación falla. Entendemos entonces que la “dificultad” está en mantener las bases de datos sincronizadas.

¿Qué haremos nosotros?

Lo que haremos será que cada vez que symfony produce un evento (nuevo usuario, usuario eliminado o cambio de contraseña) symfony se lo notificará al servidor LDAP.

Comencemos…

Antes de nada decir que uso sfDoctrineGuardPlugin para la gestión de usuarios y supongo que tienes una instalación de Symfony funcionando. Según el README del plugin tenemos que poner algo así en app.yml

all:
  sf_guard_plugin:
    check_password_callable: [ldapLogin, checkPassword]

Y por requerimientos del plugin ldapLogin (que así he llamado yo a mi clase) tiene que ser una clase estática y tener un método llamado checkPassword que devolverá verdadero o falso. Además los argumentos son usuario y contraseña (en ese orden).
Una vez hemos metido ese parámetro en app.yml creamos en /lib (si, el lib del proyecto así te sirve para todas la aplicaciones) “ldapLogin.class.php”.

Yo he aprovechado y en esa misma clase he metido los métodos de creación de usuario, modificación y borrado. El esqueleto de la clase sería algo así

class LdapLogin {

	public static function checkPassword($user,$pass){
		//Devolver verdadero o falso
	}

	public static function createUser($user,$pass){
	}

	public static function removeUser($user){
	}

	public static function updatePassword($user,$pass){

	}

}

Ya tenemos un poco en mente lo que queremos. Antes de continuar, vamos hacer nuestro sistema un poco más versátil. Como la localización, datos de administrador y todo lo relativo a LDAP puede variar vamos a hacer que esos datos sean leídos de un archivo de configuración.

Yo he utilizado el archivo config/settings.yml del proyecto y la estructura que he elegido es esta:

all:
  ldap:
     admin: uid=admin,ou=system
     password: 123456
     hostname: 127.0.0.1
     suffix: ou=users,ou=system

En admin pones el dn del admin ldap. Suffix es donde se guardan los usuarios. Esta configuración es la que se usaría si tu servidor LDAP es Apache Directory Server.

Para acceder a esa configuración podemos haceServidor LDAP Apache Directory Serverrlo de dos maneras, una es directamente a cada valor

sfConfig::get(‘sf_ldap_suffix’)

O cargando el archivo yml en una variable

$settings = sfYaml::load(sfConfig::get(‘sf_root_dir’).”/config/settings.yml”);
$suffix = $settings[‘all’][‘ldap’][‘suffix’];

Estos dos códigos hacen lo mismo. Si sólo vas a coger un dato la primera opción es mejor mientras que para varios yo prefiero la segunda pero para gustos los colores.

Ya sabemos como lo queremos hacer. Ahora queda hacerlo. Lo primero es saber cómo conectarnos a un servidor LDAP mediante php. Este código te ayudará

$ds = ldap_connect($host,$port);//Nos conectamos
if(!$ds) throw new Exception('Cant connect to LDAP host "'.$host.'"');
//Si no se pudo conectar, lazamos una excepción
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);//Usamos la versión 3, si no lo pones php or defecto usa una version mas vieja

if(!ldap_bind($ds, $admin,  $adminPass)) throw new Exception('Cant bind to LDAP host "'.$host.'"');
//Nos autenticamos como administrador. Si todo va bien la conexión esta hecha

Ahora necesitamos saber cómo insertar, modificar y elminar registros en el servidor LDAP.

//Insertar usuario en LDAP
//$user es la variable donde tenemos el nombre de usuario. Tiene que se el mismo que el nombre de usuario de symfony

                $info["sn"]= $user;
		$info["uid"]= $user;
		$info["cn"]= $user;
		$info["userPassword"] = "{SHA}". base64_encode(sha1($pass,true));
    	$info["objectclass"][]="person";
    	$info["objectclass"][]="inetOrgPerson";
    	$info["objectclass"][]="top";
    	$info["objectclass"][]="organizationalPerson";
		return ldap_add($ds, "uid=".$user.", ".sfConfig::get('sf_ldap_suffix'), $info);

Vemos que para user password usamos ciertas funciones extra. Eso es porque a mi me gusta cifrar los datos sensibles. Si usas directamente la variable $pass, ésta se guardará en texto plano.

Eliminar el usuario es más sencillo

    ldap_delete($ds, "uid=".$user.", ".sfConfig::get('sf_ldap_suffix'));

Y cambiar la contraseña también es muy fácil

$pass = array("userPassword" => "{SHA}". base64_encode(sha1($pass,true)));
		return ldap_modify ($ds, "uid=".$user.", ".sfConfig::get('sf_ldap_suffix'), $pass);

Si juntamos todo, obtenemos la clase completa

class LdapLogin {

	public static function checkPassword($user,$pass){
		$ldapUser = "uid=".$user.",".sfConfig::get('sf_ldap_suffix');		

		$ds = ldap_connect(sfConfig::get('sf_ldap_hostname'),10389);
		ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
		return @ldap_bind($ds, $ldapUser, $pass); //BIND
	}

	public static function createUser($user,$pass){
		if(sfConfig::get('sf_ldap_admin') == ""){
			$settings = sfYaml::load(sfConfig::get('sf_root_dir')."/config/settings.yml");
			$admin = $settings['all']['ldap']['admin'];
			$adminPass = $settings['all']['ldap']['password'];
			$host = $settings['all']['ldap']['hostname'];
		}else{
			$admin = sfConfig::get('sf_ldap_admin');
			$adminPass = sfConfig::get('sf_ldap_password');
			$host = sfConfig::get('sf_ldap_hostname');
		}

		$ds = ldap_connect($host,10389);
		if(!$ds) throw new Exception('Cant connect to LDAP host "'.$host.'"');
		ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);

		if(!ldap_bind($ds, $admin,  $adminPass)) throw new Exception('Cant bind to LDAP host "'.$host.'"');
		$info["sn"]= $user;
		$info["uid"]= $user;
		$info["cn"]= $user;
		$info["userPassword"] = "{SHA}". base64_encode(sha1($pass,true));
    	$info["objectclass"][]="person";
    	$info["objectclass"][]="inetOrgPerson";
    	$info["objectclass"][]="top";
    	$info["objectclass"][]="organizationalPerson";
		return ldap_add($ds, "uid=".$user.", ".sfConfig::get('sf_ldap_suffix'), $info);
	}

	public static function removeUser($user){
		if(sfConfig::get('sf_ldap_admin') == ""){
			$settings = sfYaml::load(sfConfig::get('sf_root_dir')."/config/settings.yml");
			$admin = $settings['all']['ldap']['admin'];
			$adminPass = $settings['all']['ldap']['password'];
			$host = $settings['all']['ldap']['hostname'];
		}else{
			$admin = sfConfig::get('sf_ldap_admin');
			$adminPass = sfConfig::get('sf_ldap_password');
			$host = sfConfig::get('sf_ldap_hostname');
		}

		$ds = ldap_connect($host,10389);
		if(!$ds) throw new Exception('Cant connect to LDAP host "'.$host.'"');
		ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);

		if(!ldap_bind($ds, $admin,  $adminPass)) throw new Exception('Cant bind to LDAP host "'.$host.'"');
		return ldap_delete($ds, "uid=".$user.", ".sfConfig::get('sf_ldap_suffix'));
	}

	public static function updatePassword($user,$pass){
		if(sfConfig::get('sf_ldap_admin') == ""){
			$settings = sfYaml::load(sfConfig::get('sf_root_dir')."/config/settings.yml");
			$admin = $settings['all']['ldap']['admin'];
			$adminPass = $settings['all']['ldap']['password'];
			$host = $settings['all']['ldap']['hostname'];
		}else{
			$admin = sfConfig::get('sf_ldap_admin');
			$adminPass = sfConfig::get('sf_ldap_password');
			$host = sfConfig::get('sf_ldap_hostname');
		}

		$ds = ldap_connect($host,10389);
		if(!$ds) throw new Exception('Cant connect to LDAP host "'.$host.'"');
		ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);

		if(!ldap_bind($ds, $admin,  $adminPass)) throw new Exception('Cant bind to LDAP host "'.$host.'"');

		$pass = array("userPassword" => "{SHA}". base64_encode(sha1($pass,true)));
		return ldap_modify ($ds, "uid=".$user.", ".sfConfig::get('sf_ldap_suffix'), $pass);

	}

}

Desde cualquier parte de symfony podremos usar esta clase.

Para terminar vamos a poner la guinda al pastel. Cuando se cree el usuario en symfony, también haremos que se cree en el sevidor LDAP.

En mi aplicación la creación de un usuario es algo bastante complejo y por ese motivo uso el task create-user de sfGuard. Por supuesto que puedes modificar la clase sfGuardUser para que cuango haga insert o update ejecute el código para insertar el usuario pero yo voy a exponer mi método.

Abrimos el archivo plugins/sfDoctrineGuardPlugin/lib/task/sfGuardCreateUserTask.class.php
En el método execute insertamos justo al principio este código

    if(!LdapLogin::createUser($arguments['username'],$arguments['password']))
        throw new Exception('User already exists in LDAP database');

Creo que estas líneas se explican por sí solas. Intenta crear el usuario en el servidor LDAP. Si ya existe lanza una excepción tirando abajo el proceso de creación del usuario.

Lo ideal es que crees otros task para actualizar y borrar el usuario ya que si te limitas a hacer un delete al user sólo borrarás al usuario de la base de datos de symfony pero no del servidor LDAP. Aunque como dije antes, puedes hacer que las acciones de update y delete de un objeto Doctrine (ORM usado por Symfony) hagan más cosas. Probablemente otro día te diga como hacerlo.

Nos vemos!

You may also like...

1 Response

  1. Lienys dice:

    Muy bueno el post, ahora me pondre a probarlo, pero a grandes razgos responde todas mis dudas.

    gracias por sacar tiempo y postear cosas tan interesantes!!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *