This is a quick post about how to change a password in Active Directory using the Symfony LDAP component, since I hadn’t run across any examples for that online.

Initial Setup

Before we can do anything, make sure you have the php_ldap extension installed and configured. Next you will need to setup Symfony, if you don’t already have a site up and running or a code base running. Refer to here to set that up https://symfony.com/doc/current/setup.html

After you have Symfony setup, install the LDAP component with the following command:

composer require symfony/ldap

Create Our Classes

/src/Factory/LdapConnectionFactory.php

<?php
namespace App\Factory;

use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\LdapInterface;

class LdapConnectionFactory
{
    public static function create(array $connectionDetails): LdapInterface
    {
        $ldap = Ldap::create(
            'ext_ldap',
            [
                'connection_string' => $connectionDetails['connection_string'],
                'version' => $connectionDetails['version'],
                'encryption' => $connectionDetails['encryption']
            ]
        );

        $ldap->bind($connectionDetails['bind.user'], $connectionDetails['bind.password']);


        return $ldap;
    }
}

This factory will be used to provide an LDAP connection based on the connection details we give it.

Next we will create a Service class that uses our Factory.

/src/Service/LdapService.php

<?php
namespace App\Service;

use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\LdapInterface;

class LdapService
{
    public function __construct(private LdapInterface $connection, private string $baseDn)
    {
    }

    public function setAccountPassword(string $samAccountName, string $password): bool
    {
        //get the account
        $em = $this->connection->getEntryManager();
        $query = $this->connection->query($this->baseDn, "(&(objectClass=User)(sAMAccountName=$samAccountName))");
        $result = $query->execute();

        //if the account was found
        if (count($result) > 0) {
            $entry = $result[0];

            //make an new entity just storing want to update.
            $newEntry = new Entry($entry->getAttribute("distinguishedName")[0], [
                'unicodePwd' => [iconv("UTF-8", "UTF-16LE", '"' . $password . '"')]
            ]);
            $em->update($newEntry);
            return true;
        }

        return false;
    }
}

The LdapService is pretty bare, but we only really want it for setting a password right now anyway. So, the sole function this service provides is setAccountPassword. Through this function we can tell Symfony the sAMAccountName of the account we want to update and the password to set on the account.

The function will look for the entity in AD and if found, it will pull the distinguished name on the Entry to make a new Entry object just containing the changes we want to make. We do this because if we try to use the returned Entry object from our query, the Entity Manager tries to do things beyond the scope of just changing the password when we tell the Entity Manager to update the Entry.

For example, if we use the returned Entry object and you gave the binding account rights to just change and reset the password for accounts under the Organizational Unit your base DN is, it will thrown an insufficient permissions error. Further more if you give the bind account full access, it will then throw a constraint violation. So, we do it this way instead.

You’ll also note that for the new password, we are using iconv to encode the password as UTF-16LE. There are other ways to do this, but this way worked for me.

Next we will move on to the configuration used for this setup.

Configuration

First we will need to make the parameters.yaml that our LdapConnectionFactory will be using. You don’t have to do this, you could hard code these values, or use Symfony’s Vault.

parameters:
  ldap:
    connection_string: ldaps://yoursever.example.com:636
    version: 3
    encryption: ssl
    bind.user: user dn to bind with
    bind.password: users password
  ldap.base_dn: base search OU

Next we will want to tell our services.yaml about how to autowire our factory and service.

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    App\Factory\LdapConnectionFactory:
        factory: [ null, 'create' ]
        arguments:
            $connectionDetails: '%ldap%'

    App\Service\LdapService:
        arguments:
            $connection: '@App\Factory\LdapConnectionFactory'
            $baseDn: '%ldap.base_dn%'

Example Usage

<?php
namespace App\Controller;

use App\Service\LdapService;
use App\Utility\PasswordGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class DefaultController extends AbstractController
{
    
    #[Route('/change_password')]
    public function example(LdapService $service, PasswordGenerator $passwordGenerator): Response
    {
        $newPassword = PasswordGenerator::generatePassword();
        if ($service->setAccountPassword('example_acount', $newPassword)) {
            return new JsonResponse(['success' => true, 'message' => ' password changed.']);
        }
        
        return new JsonResponse(['error' => true, 'message' => 'password not changed.'], 400);
    }
}

The above is just a quick example of changing and account password using a route in the DefaultController. The PasswordGenerator is just a utility that generates a password string.