Last months I were working on a new Symfony application where the users needed to be authenticated against a Windows Active Directory.
Our custom login authentication process will do this:
- User sign in through a login form
- We connect to our LDAP server and check if user credentials are correct.
- If credentials are correct, we check if the user exists on our database user table.
- If the user exists, we update the last login field, else we create the new user on database user table.
- We log the user into our Symfony application.
First step is to create a service class for authenticating users with LDAP:
<?php
// src/Services/Utils/Ldap.php
namespace Services\Utils;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class Ldap
{
private $em, $request, $container;
private $strLdapServer, $strLdapDN;
private $objLdapBind, $strLdapFilter, $strLdapDC, $objLdapConnection;
private $arrLoginResult, $strUserEmail, $strUserPasswd;
public function __construct(ContainerInterface $container, Request $request)
{
$this->request = $request;
$this->container = $container;
// LDAP CONFIG
$this->strLdapServer = "192.168.1.2";
$this->strLdapDN = "DomainName";
$this->strLdapDC = "dc=DcName,dc=local";
// init vars
$this->objLdapBind = false;
$this->objLdapConnection = false;
}
// Load LDAP config
private function loadLdapConfig()
{
$this->strLdapFilter = "(sAMAccountName=" . $this->strUserEmail . ")";
$this->strLdapServer = "ldap://" . $this->strLdapServer;
$this->strLdapDN = $this->strLdapDN . "\\" . $this->strUserEmail;
}
// Connects to LDAP server
private function connectToLdapServer()
{
$this->objLdapConnection = ldap_connect($this->strLdapServer);
ldap_set_option($this->objLdapConnection, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->objLdapConnection, LDAP_OPT_REFERRALS, 0);
$this->objLdapBind = @ldap_bind($this->objLdapConnection, $this->strLdapDN, $this->strUserPasswd);
}
// Get username and password form login form
private function getLdapUsernameAndPassword()
{
$this->strUserEmail = $this->request->request->get('email');
$this->strUserPasswd = $this->request->request->get('password');
if( ! empty($this->strUserEmail) && ! empty($this->strUserPasswd))
{
$this->arrLoginResult['USER_EMAIL'] = $this->strUserEmail;
$this->arrLoginResult['PASSWORD'] = $this->strUserPasswd;
// get only username, deleting all data after @
if(preg_match('/@/', $this->strUserEmail))
{
$arrUserData = explode("@", $this->strUserEmail);
$this->strUserEmail = $arrUserData[0];
}
}
else
{
$this->arrLoginResult['ERROR'] = "EMPTY_CREDENTIALS";
}
}
// check ldap login with username and password
public function checkLdapLogin()
{
$this->arrLoginResult = array(
'LOGIN' => 'ERROR',
'ERROR' => 'INIT',
'USER_EMAIL' => NULL,
'PASSWORD' => NULL,
'USERNAME' => NULL
);
// get username and password
$this->getLdapUsernameAndPassword();
if( ! empty($this->strUserEmail) && ! empty($this->strUserPasswd) )
{
// load LDAP config
$this->loadLdapConfig();
// connect to server
$this->connectToLdapServer();
// check connection result
if($this->objLdapBind)
{
// login ok
$this->arrLoginResult['LOGIN'] = "OK";
// get ldap response
$result = ldap_search($this->objLdapConnection, $this->strLdapDC, $this->strLdapFilter);
// sort ldap results
ldap_sort($this->objLdapConnection, $result, "sn");
// get user info
$info = ldap_get_entries($this->objLdapConnection, $result);
// get user info
$this->arrLoginResult['USERNAME'] = ! empty($info[0]['name'][0]) ? $info[0]['name'][0] : NULL;
// close ldap connection
@ldap_close($this->objLdapConnection);
// login user
$objUserServ = $this->container->get('userManager');
$objUserServ->loginAction($this->strUserEmail);
}
else
{
$this->arrLoginResult['ERROR'] = 'INVALID_CREDENTIALS';
}
}
return json_encode($this->arrLoginResult);
}
}
Our Ldap service checks autenthication using the user email and password entered in the login form against our Windows Active Directory.
With the Ldap service created, we need to include it into the app/config/Services.yml file.
# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
services:
ldap:
class: Services\Utils\Ldap
arguments: ["@service_container", "@request"]
scope: request
Now, we are going to create our login controller:
<?php
// src/UserBundle/Controller/LoginController.php
namespace UserBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class LoginController extends Controller
{
public function indexAction(Request $request)
{
$arrViewData = array('USER_EMAIL' => NULL, 'PASSWORD' => NULL, 'ERROR' => NULL);
// Checks if the login form has been submitted
if($request->getMethod() == 'POST')
{
// load Ldap service
$objLdapServ = $this->get('ldap');
// check Ldap login
$arrLoginResult = $objLdapServ->checkLdapLogin();
// Ldap login result
$arrViewData = json_decode($arrLoginResult, TRUE);
// check Ldap login result
if($arrViewData['LOGIN'] == "OK")
{
// user logged ok, then we redirect to the home page
$router = $this->get('router');
$url = $router->generate('home');
return $this->redirect($url);
}
}
return $this->render('UserBundle:Login:login.html.twig', $arrViewData);
}
}
And the login view:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body class="login-page">
<div class="container">
<div class="login-box">
{% if ERROR == 'INVALID_CREDENTIALS' %}
<div class="row">
<div class="alert alert-danger text-center">
<strong>Error, wrong credentials.</strong>
</div>
</div>
{% endif %}
<div class="login-box-body">
<form class="form-signin" action="?checkLogin" method="post">
<div class="form-group has-feedback">
<input name="email" type="text" class="form-control" placeholder="User or email" value="{{ USER_EMAIL }}" autofocus />
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="Password" name="password" value="{{ PASSWORD }}" />
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-12">
<button type="submit" class="btn btn-primary btn-block btn-flat">Login</button>
</div>
</div>
</form>
</div>
<!-- /.login-box-body -->
</div>
</div><!-- /container -->
</body>
</html>
After that, we are going to create a ‘UserManager‘ service class for managing our application users. In this service we handle the user session, create new users, etc.
<?php
// src/Services/User/UserManager.php
namespace Services\User;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\ORM\EntityManager;
use UserBundle\Entity\User;
class UserManager
{
private $container, $em, $request, $session, $user;
public function __construct(EntityManager $em, ContainerInterface $container, Request $request, Session $session)
{
$this->em = $em;
$this->request = $request;
$this->container = $container;
$this->session = $session;
}
// checks if user exists when login form has been submitted
public function loginAction($strUsername)
{
if( ! $this->checkUserExists($strUsername) )
{
// create new user
$this->createUser($strUsername);
}
$this->createLoginSession();
}
// get user data from database
public function getUser($strUsername)
{
return $this->em->getRepository('UserBundle:User')->findOneBy( array('user' => $strUsername) );
}
// Check if a user exists on database
public function checkUserExists($strUsername)
{
$this->user = $this->getUser($strUsername);
return ( ! empty($this->user)) ? true : false;
}
// Create new user on database
public function createUser($strUsername)
{
$boolResult = false;
$objCurrentDatetime = new \Datetime();
try
{
$objUser = new User();
$objUser->setUser($strUsername);
$objUser->setCreationDate($objCurrentDatetime);
$objUser->setLastLoginDate($objCurrentDatetime);
// save data
$this->em->persist($objUser);
$this->em->flush();
// result data
$boolResult = true;
// user obj
$this->user = $objUser;
}
catch(Exception $ex)
{
echo $ex->getMessage();
}
return $boolResult;
}
// creates login session
public function createLoginSession()
{
$objToken = new UsernamePasswordToken($this->user, null, 'main', $this->user->getRoles() ) ;
// update user last login
$this->user->setLastLoginDate( new \Datetime() );
$this->em->persist($this->user);
$this->em->flush();
// save token
$objTokenStorage = $this->container->get("security.token_storage")->setToken($objToken);
$this->session->set('_security_main', serialize($objToken));
}
// logout a user
public function logOutUser()
{
$this->container->get('security.context')->setToken(null);
$this->container->get('request')->getSession()->invalidate();
$url = $router->generate('oportunidades');
return $this->redirect($url);
}
}
Add UserManager service into app/config/services.yml
userManager:
class: Services\User\UserManager
arguments: ["@doctrine.orm.entity_manager", "@service_container", "@request", "@session"]
scope: request
We need to configure our app/config/security.yml file:
# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
db_provider:
entity:
class: UserBundle:User
property: user
#main:
#id: securityProvider
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/login$
security: false
#anonymous: ~
#http_basic:
# realm: "Secured Demo Area"
main:
anonymous: ~
form_login:
login_path: login
check_path: login
# activate different ways to authenticate
# http_basic: ~
# http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
# form_login: ~
# http://symfony.com/doc/current/cookbook/security/form_login_setup.html
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
access_control:
- { path: ^/, roles: ROLE_USER }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Our UserEntity should be something like this:
UserBundle\Entity\User:
type: entity
table: null
repositoryClass: UserBundle\Repository\UserRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
user:
type: string
length: 255
unique: true
role:
type: string
nullable: true
column: role
lastLoginDate:
type: datetime
nullable: true
column: last_login_date
creationDate:
type: datetime
nullable: true
column: creation_date
deletionDate:
type: datetime
nullable: true
column: deletion_date
lifecycleCallbacks: { }
Our entity has a field named ‘role’ where we can save the user role, but you will need to implement a method to save and load the role when user logs into the application. This topic could be interesting for a new post.
A young developer from Madrid who loves programming and computing. Constantly testing with new technologies and thinking in new projects and challenges.
