Archivo de la categoría: Symfony

Config TypeScript and create aliases in Webpack Encore from Symfony

In the following article we will learn how to configure and create aliases in Webpack Encore from Symfony to be able to use TypeScript in our projects. We will also learn how to configure Webpack aliases that point to directories in our project in order to import our TypeScript classes easily without having to use relative paths.

The first thing we have to do is to install the tsconfig-paths-webpack-plugin package with NPM:

npm i --save tsconfig-paths-webpack-plugin

Once installed, we are going to create the tsconfig.json file (in our case we are going to do it inside the /assets folder that is where we have all the assets of our project) with the following content.

{
  "compilerOptions": {
    "sourceMap": true,
    // Last ECMAScript version.
    "target": "esnext",
    // Search in node_modules for non-relative import.
    "moduleResolution": "node",
    // Enables strict mode for settings such as strictNullChecks & noImplicitAny.
    "strict": true,
    "jsx": "react",
    "isolatedModules": true,
    "esModuleInterop": true,
    "baseUrl": "./ts/",
    "paths": {
      "@ts/*": ["*"]
    }
  }
}

Important properties to consider:

  • baseUrl: the relative path where our TypeScript files will be.
  • paths: the paths that will have the aliases that we will define later in Webpack.

Now we need to add the plugin to the Webpack Encore configuration. So let’s edit the webpack.config.js file located in the root of our Symfony project.

In the following example we will create several aliases, so we will need to have the Path plugin installed.

const Encore = require('@symfony/webpack-encore');
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

const ROOT_PATH = path.resolve(__dirname, './');
const ASSETS_PATH = ROOT_PATH + '/assets';

const ALIASES = {
    '@assets' : ASSETS_PATH,
    '@ts' : ASSETS_PATH + '/ts',
    '@styles' : ASSETS_PATH + '/styles'
};

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')

    // Here we tell Webpack that we want to have these aliases available
    .addAliases(ALIASES)

    // Here there would be more configuration details that we are going to omit
    // ...
;

Now, at the end of our webpack.config.js file we have to add the following code:

var config = Encore.getWebpackConfig()

config.resolve.plugins = [
    new TsconfigPathsPlugin({
        configFile: './assets/tsconfig.json',
        extensions: ['.jsx', '.json', '.ts', '.tsx']
    })
]

module.exports = config

There we indicate where our configuration file tsconfig.json is located and the extensions we want to accept.

Once this is done, we can use the aliases defined in Webpack as follows:

import {BaseController} from "@ts/Controller/BaseController"
import {ImportedClass} from "@ts/ImportedClass"

class MyFirstController extends BaseController {
    execute(): void {
        let importedClass = new ImportedClass()
        importedClass.execute()
    }
}

export {MyFirstController}

And that would be all.

Any questions, see you in the comments 😉

Custom login with LDAP in Symfony

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.