Search code examples
symfonysecurityldapsymfony-3.3

Symfony Multiple Ldap providers


I am not able to validate a user against more than one ldap library using the services and security config (Symfony 3.3).

I am using the Ldap symfony component and creating 2 ldap config services for two different hosts.

services.yml:

ldap1:
    class: Symfony\Component\Ldap\Ldap
    arguments: ['@ldap_adapter1']
ldap_adapter1:
    class: Symfony\Component\Ldap\Adapter\ExtLdap\Adapter
    arguments:
        -   host: serldap.abc.fr
            port: 389
            options:
                protocol_version: 3
                referrals: false
ldap2:
    class: Symfony\Component\Ldap\Ldap
    arguments: ['@ldap_adapter2']
ldap_adapter2:
    class: Symfony\Component\Ldap\Adapter\ExtLdap\Adapter
    arguments:
        -   host: ldap.xyz.fr
            port: 389
            options:
                protocol_version: 3
                referrals: false

security.yml:

security:
    providers:
        chain_provider:
            chain:
                providers: [ldap_1, ldap_2]
        ldap_1:
            ldap:
                service: ldap1
                base_dn: ou=abcaccount,dc=abc,dc=fr
                search_dn: uid=a1,ou=abcaccount,dc=abc,dc=fr
                search_password: pass1
                default_roles: ROLE_USER
                uid_key: uid
        ldap_2:
            ldap:
                service: ldap2
                base_dn: ou=xyzaccount,dc=xyz,dc=fr
                search_dn: uid=a2,ou=xyzaccount,dc=xyz,dc=fr
                search_password: pass2
                default_roles: ROLE_USER
                uid_key: uid
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            pattern: ^/
            anonymous: ~
            provider: chain_provider
            form_login_ldap:
                login_path: login
                check_path: login


    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_USER }

If i add the dn_string under the form_login_ldap. i,e:

dn_string: 'uid={username},ou=xyzaccount,dc=xyz,dc=fr'

This works, the problem is this can only be configured for one Ldap. without this line I get the following error:

php.DEBUG: Warning: ldap_bind(): Unable to bind to server: Invalid DN syntax

2 questions:

  1. Is there any way of verifying a user against 2 ldap libraries whilst keeping it simple?

  2. It would be even better if they could choose the library they where validating against via the login form with this being passed through the as some kind of input?

e.g.

dn_string: 'uid={username},ou={chosenOUInForm},dc={chosenDC1InForm},dc={chosenDC2InForm}'

Thanks in advance.


Solution

  • I managed to get it working by adding the request_matcher param to the security:

    security:
        providers:
            chain_provider:
                chain:
                    providers: [ldap_1, ldap_2]
            ldap_1:
                ldap:
                    service: ldap1
                    base_dn: ou=abcaccount,dc=abc,dc=fr
                    search_dn: ~
                    search_password: ~
                    default_roles: ROLE_USER
                    uid_key: uid
            ldap_2:
                ldap:
                    service: ldap2
                    base_dn: ou=xyzaccount,dc=xyz,dc=fr
                    search_dn: ~
                    search_password: ~
                    default_roles: ROLE_USER
                    uid_key: uid
    
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
    
    
            base:
                pattern: ^/
                request_matcher: app.base_firewall_matcher
                anonymous: ~
                form_login_ldap:
                    service: ldap1 # this doesn't matter for the base firewall as it is never passed with no check_path
                    login_path: login
    
    
            one:
                pattern: ^/
                request_matcher: app.first_firewall_matcher
                anonymous: ~
                provider: ldap_1
                form_login_ldap:
                    service: ldap1
                    login_path: login
                    check_path: login_1_check
                    dn_string: 'uid={username},ou=abcaccount,dc=abc,dc=fr'
    
            two:
                pattern: ^/
                request_matcher: app.second_firewall_matcher
                anonymous: ~
                provider: ldap_2
                form_login_ldap:
                    service: ldap2
                    login_path: login
                    check_path: login_2_check
                    dn_string: 'uid={username},ou=xyzaccount,dc=xyz,dc=fr'
    
        access_control:
            - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/, roles: ROLE_USER }
    

    Where the form page (twig page that the login path returns) will have one button to submit the form to login_1_check and one button to submit the form to login_2_check:

     <form action="{{ path('login_1_check') }}" method="post">
                    <input type="text" id="username" name="_username"/>
                    <input type="password" id="password" name="_password" />
    	<button type="submit">login to LDAP 1</button>
        <button type="submit" formaction="{{ path('login_2_check') }}">login to LDAP 2</button>
    </form>

    The firewalls are hit in order, therefore the base firewall is checked first and if true is returned (if the user is not already authenticated or the login form is not being submitted) it goes to the login path.

    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\RequestMatcherInterface;
    use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
    
    
    class BaseFirewallMatcher implements RequestMatcherInterface
    {
    
        // Should only be matched on login page when user first arrives to GET the form, if already validated with one
        // of the other two security providers, they will be checked against their respective firewalls.
        public function matches(Request $request){
    
            $session = $request->getSession();
            $oneValidated = $session->get("_security_one", null);
            $twoValidated = $session->get("_security_two", null);
    
    
            // If already validated with another security provider
            if($oneValidated or $twoValidated){
                return false;
            }
            else{
    
                $url = $request->getPathInfo();
                // If these two logins are not matched
                if($url != "/login_1_check" && $url != "/login_2_check"){
                    return true;
                }
                else{
    
                    return false;
                }
    
            }
    
        }
    }
    

    When a user submits by pressing one of the buttons the base firewall will not be triggered as the BaseFirewallMatcher will return false. The 1st LDAP check is then done going to the following request matcher.

    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\RequestMatcherInterface;
    use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
    
    class FirstFirewallMatcher implements RequestMatcherInterface
    {
    
        public function matches(Request $request){
    
    
            $session = $request->getSession();
            $oneValidated = $session->get("_security_one", null);
            $twoValidated = $session->get("_security_two", null);
    
    
    
            if($twoValidated){
                return false;
            }
    
            else{
                if($oneValidated){
                    return true;
                }
                $url = $request->getPathInfo();
                if ($url == "/login_1_check"){
                    return true;
                }
                else{
                    return false;
                }
            }
    
        }
    }
    

    This returns true, i.e. if user is already authenticated with ldap1 or the user has chosen to login to ldap1. The firewall is then triggered.

    If this returns false e.g. when the user chooses to login to ldap2 or is already authenticated with ldap2 - it goes to the next request matcher:

    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\RequestMatcherInterface;
    use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
    
    class SecondFirewallMatcher implements RequestMatcherInterface
    {
    
    
        public function matches(Request $request){
    
            $session = $request->getSession();
            $oneValidated = $session->get("_security_one", null);
            $twoValidated = $session->get("_security_two", null);
    
    
    
            if($oneValidated){
                return false;
            }
            else{
                if($twoValidated){
                    return true;
                }
                $url = $request->getPathInfo();
                dump($url);
    
                if ($url == "/login_2_check"){
                    return true;
                }
                else{
                    return false;
                }
            }
    
        }
    }
    

    Remember to setup the services for each request_matcher:

    app.base_firewall_matcher:
        class: {pathtofirewallmatcher}\BaseFirewallMatcher
    app.first_firewall_matcher:
        class: {pathtofirewallmatcher}\FirstFirewallMatcher
    app.second_firewall_matcher:
        class: {pathtofirewallmatcher}\SecondFirewallMatcher
    

    The code might not be perfect as in the end I moved onto a different type of authentication. But this seemed to work at the time and I assume you can add as many ldaps as required.