Lightweight Directory Access Protocol is originally a protocol for querying and modifying directory services. This protocol is based on TCP/IP.
$ composer require symfony/ldap
# LDAP / AD host
LDAP_HOST=
# LDAP / AD port
LDAP_PORT=
# LDAP / AD base dn ("cn=example,cn=com")
LDAP_BASEDN=
# LDAP / AD search user
LDAP_SEARCHDN=
# LDAP / AD search user password
LDAP_SEARCHPASSWORD=
# LDAP / AD encryption method
LDAP_ENCRYPT=
# LDAP / AD authorized groups
# '["group","group"]'
# Optionnal if you accept all ldap users
LDAP_AUTHORIZED_GROUPS=
config/services.yaml
services:
Symfony\Component\Ldap\Ldap:
arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
tags:
- ldap
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: "%env(resolve:LDAP_HOST)%"
port: "%env(resolve:LDAP_PORT)%"
encryption: "%env(resolve:LDAP_ENCRYPT)%"
options:
protocol_version: 3
referrals: false
src/Security/CustomLdapUserProvider.php
<?php
namespace App\Security;
/* ... */
class CustomLdapUserProvider extends LdapUserProvider
{
private $defaultRoles;
private $passwordAttribute;
private $extraFields = array();
private $authorizedGroups;
public function __construct(
LdapInterface $ldap,
string $baseDn,
string $searchDn = null,
string $searchPassword = null,
array $authorizedGroups = null,
array $defaultRoles = [],
string $uidKey = null,
string $filter = null,
string $passwordAttribute = null,
array $extraFields = []
){
$this->authorizedGroups = $authorizedGroups;
parent::__construct($ldap, $baseDn, $searchDn, $searchPassword, $defaultRoles, $uidKey, $filter, $passwordAttribute, $extraFields);
}
protected function loadUser(string $username, Entry $entry)
{
$password = null;
$extraFields = [];
if (null !== $this->passwordAttribute) {
$password = $this->getAttributeValue($entry, $this->passwordAttribute);
}
foreach ($this->extraFields as $field) {
$extraFields[$field] = $this->getAttributeValue($entry, $field);
}
if ($this->authorizedGroups) {
$isAuthorized = false;
foreach ($entry->getAttribute("memberOf") as $ldapGroupDn) {
$isAuthorized = $isAuthorized || in_array($ldapGroupDn, $this->authorizedGroups);
}
if (!$isAuthorized) {
throw new NotAuthorizedException();
}
}
$results = array("ROLE_USER");
foreach ($entry->getAttribute("memberOf") as $ldapGroupDn) {
$results[] = "ROLE_" . ldap_explode_dn($ldapGroupDn, 1)[0];
}
if (!empty($results))
$roles = $results;
else
$roles = $this->defaultRoles;
return new LdapUser($entry, $username, $password, $roles, $extraFields);
}
private function getAttributeValue(Entry $entry, string $attribute)
{
if (!$entry->hasAttribute($attribute)) {
throw new InvalidArgumentException(sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
}
$values = $entry->getAttribute($attribute);
if (1 !== \count($values)) {
throw new InvalidArgumentException(sprintf('Attribute "%s" has multiple values.', $attribute));
}
return $values[0];
}
}
src/Security/NotAuthorizedException.php
<?php
namespace App\Security;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class NotAuthorizedException extends AuthenticationException
{
/**
* {@inheritdoc}
*/
public function getMessageKey(): string
{
return "Your not authorized message";
}
}
config/packages/security.yaml
security:
enable_authenticator_manager: true
providers:
ldap_users:
id: App\Security\CustomLdapUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
provider: ldap_users
guard:
authenticators:
- App\Security\LdapFormAuthenticator
logout:
path: logout
target: login
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
src/Security/LdapFormAuthenticator.php
<?php
namespace App\Security;
/* ... */
class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $urlGenerator;
private $csrfTokenManager;
protected $ldap;
public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, Ldap $ldap)
{
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->ldap = $ldap;
}
public function supports(Request $request)
{
return 'login' === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $userProvider->loadUserByIdentifier($credentials['username']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
try {
$this->ldap->bind($user->getEntry()->getDn(), $credentials['password']);
} catch (ConnectionException $e) {
return false;
}
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('index'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('login');
}
}
src/Controller/AuthController.php
<?php
namespace App\Controller;
/* */
class AuthController extends AbstractController
{
/**
* @Route("/login", name="login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error
]);
}
/**
* @Route("/logout", name="logout")
* @throws Exception
*/
public function logout()
{
throw new Exception('This method can be blank - it will be intercepted by the logout key on your firewall');
}
}
templates/security/login.html.twig
<form action="{{ path('login') }}" method="POST">
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<div>
<label for="_username" class="text-lg">Username</label>
<input type="text" id="_username" name="_username" placeholder="Username" />
</div>
<div>
<label for="_password" class="text-lg">Password</label>
<input type="text" id="_password" name="_password" placeholder="Password" />
</div>
<div>
<button type="submit">Login</button>
</div>
<input
type="hidden"
name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
/>
</form>