Working on my local Ubuntu 20.04 installation, I will set up a development environment and implement Symfony authentication with LDAP.
Citing from the the documentation I’m interested in this scenario:
Checking a user’s password against an LDAP server while fetching user information from another source (database using FOSUserBundle, for example).
Setup Symfony and create new project as per symfony.com/download:
# Make sure you have PHP extensions we will use and ldapsearch.
sudo apt install php-ldap php-sqlite3 ldap-utils
# Create new Symfony project.
symfony new ldap_test --version=5.4 --full
cd ldap_test
composer require symfony/ldap
To make it simple, I will use sqlite to store the user records, just uncomment in .env:
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
Create user entity. Answer with all defaults just for “Does this app need to hash/check user passwords?” I said no.
php bin/console make:user
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Create controller from a template:
php bin/console make:controller Protected
In config/packages/security.yaml make the firewall to protect the access to our newly created action:
access_control:
- { path: ^/protected, roles: ROLE_USER }
Run the server and make sure that the access to http://127.0.0.1:8000/protected is not allowed:
Let’s create sample user record in the database
sqlite> .schema user
CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json)
);
CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON user (email);
sqlite> INSERT INTO user VALUES (NULL,'test@example.invalid','[ROLE_USER]');
We will use Apache Directory Studio to run local LDAP server. Download it from (directory.apache.org/studio/downloads.html)[https://directory.apache.org/studio/downloads.html], extract and run.
Click on the “LDAP servers” tab, create new server.
Then start it.
When you double-click it, you can see that its running on port 10389:
Right click it and “Create a connection”. Let’s create sample user. Download Sample LDAP LDIF file. In LDAP browser tab, right-click the root node, select import and choose the file.
Let’s test the connection using ldapsearch:
$ ldapsearch -H ldap://localhost:10389 -D 'cn=test@example.invalid,dc=example,dc=com' -w xyz sn=Muras
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: sn=Muras
# requesting: ALL
#
# Tomek Muras, example.com
dn: cn=Tomek Muras,dc=example,dc=com
mail: test@example.invalid
sn: Muras
cn: Tomek Muras
objectClass: top
objectClass: inetOrgPerson
objectClass: person
objectClass: organizationalPerson
userPassword:: e1NTSEF9WmtQNm9NTGFTK1hiZ2c5RG1WODg4T2FmaktUTXhCNkpGSEVrUWc9PQ=
=
uid: tmuras
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
Add login form, as per symfony.com/doc/5.4/security.html. I have put login action in the same ProtectedController:
/**
* @Route("/login", name="login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('protected/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
Create service in Symfony, based on symfony.com/doc/5.4/security/ldap.html, add to config/services.yaml:
Symfony\Component\Ldap\Ldap:
arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
tags:
- ldap
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: localhost
port: 10389
encryption: false
options:
protocol_version: 3
referrals: false
Update config/packages/security.yaml:
security:
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
form_login_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string: 'cn={username},dc=example,dc=com'
login_path: login
check_path: login
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/protected, roles: ROLE_USER }
Now we can access the protected resource, once valid LDAP password is entered:
The relevant logs during the login:
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39400 Accepted
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39400 [302]: POST /login
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39400 Closing
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39404 Accepted
[Web Server ] Dec 12 16:03:34 |INFO | SERVER POST (302) /login host="127.0.0.1:8004" ip="127.0.0.1" scheme="https"
[Web Server ] Dec 12 16:03:34 |INFO | SERVER GET (200) /protected
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39404 [200]: GET /protected
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39404 Closing
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39406 Accepted
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39406 [200]: GET /_wdt/f011c6
[Web Server ] Dec 12 16:03:34 |INFO | SERVER GET (200) /_wdt/f011c6 ip="127.0.0.1"
[PHP ] [Sun Dec 12 16:03:34 2021] 127.0.0.1:39406 Closing
[Application] Dec 12 16:03:34 |INFO | REQUES Matched route "login". method="POST" request_uri="http://127.0.0.1:8000/login" route="login" route_parameters={"_controller":"App\\Controller\\ProtectedController::login","_route":"login"}
[Application] Dec 12 16:03:34 |DEBUG | SECURI Checking for authenticator support. authenticators=1 firewall_name="main"
[Application] Dec 12 16:03:34 |DEBUG | SECURI Checking support on authenticator. authenticator="Symfony\\Component\\Ldap\\Security\\LdapAuthenticator"
[Application] Dec 12 16:03:34 |DEBUG | DOCTRI SELECT t0.id AS id_1, t0.email AS email_2, t0.roles AS roles_3 FROM user t0 WHERE t0.email = ? LIMIT 1 0="test@example.invalid"
[Application] Dec 12 16:03:34 |INFO | PHP User Deprecated: Since symfony/ldap 5.3: Not implementing the "Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface" interface in class "App\Entity\User" while using password-based authenticators is deprecated.
[Application] Dec 12 16:03:34 |INFO | PHP User Deprecated: Since symfony/security-http 5.3: Not implementing the "Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface" interface in class "App\Entity\User" while using password-based authentication is deprecated.
[Application] Dec 12 16:03:34 |INFO | SECURI Authenticator successful! authenticator="Symfony\\Component\\Security\\Http\\Authenticator\\Debug\\TraceableAuthenticator" token={"Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken":"UsernamePasswordToken(user=\"test@example.invalid\", authenticated=true, roles=\"ROLE_USER\")"}
[Application] Dec 12 16:03:34 |DEBUG | SECURI The "Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator" authenticator set the response. Any later authenticator will not be called
[Application] Dec 12 16:03:34 |DEBUG | SECURI Stored the security token in the session. key="_security_main"
The full code is available on github.com/tmuras/symfony-ldap-authentication.