-->

Symfony LDAP auth bind with username and password

2019-05-31 13:15发布

问题:

I am trying to get LDAP authentication working for Symfony firewall but am having trouble. The main issue seems to stem from the Symfony LdapUserProvider - it does not take the user supplied username and password when trying ldap_bind().

So, I have this as my firewall config:

$app->register(new SilexProvider\SecurityServiceProvider(), [
    'security.firewalls' => [
        'secured' => [
            'pattern' => '^.*$',
            'http' => true,
            'users' => new \Symfony\Component\Security\Core\User\LdapUserProvider(
                \Symfony\Component\Ldap\Ldap::create('ext_ldap', [
                    'connection_string' => 'ldap://MY_LDAP_DOMAIN',
                ]),
                'dc=MY_DC_1,dc=MY_DC_2',
                'uid={username},cn=users,cn=accounts,dc=MY_DC_1,dc=MY_DC_2'
            ),
        ],
    ],
]);

But my {username} part is not replaced by the user supplied username when the ldap_bind method is called. So, the dn string passed to ldap_bind is literally uid={username},cn=users,cn=accounts,dc=MY_DC_1,dc=MY_DC_2 - the username isn't replaced.

If I look through the code though this is expected, as LdapUserProvider->loadUserByUsername() calls bind before doing any string replacements. The other problem is that it does not know the user supplied password until much later, so the bind call again doesn't have the user supplied password.

How can I set this up so that it will replace my dn and password appropriately? If I use these 2 basic lines (where $data is an array of a valid user):

$ldap = ldap_connect('MY_LDAP_DOMAIN');
$bind = ldap_bind($ldap, 'uid=' . $data['username'] . ',cn=users,cn=accounts,dc=MY_DC_1,dc=MY_DC_2', $data['password']);

Then it binds perfectly. How can I translate those 2 lines into a situation that Symfony firewall understands?

回答1:

You have 2 main issues in the existing code:

  1. Symfony's LdapUserProvider component by default uses Active Directory (Windows) schema: sAMAccountName={username} instead of Open LDAP's uid={username}
  2. You're using built-in http security firewall, which by default uses DaoAuthenticationProvider authentication provider . In case of LDAP authentication, you need to use LdapBindAuthenticationProvider instead.

The first issue can be solved by passing user identifier key to LdapUserProvider:

$app['ldap.users'] = function () use ($app) {
    return new LdapUserProvider(
        // your LDAP adapter
        $app['ldap'],
        // base DN
        'dc=example,dc=com',
        // you don't need search DN
        null,
        // you don't need search password
        null,
        // list of default roles, can be empty array
        ['ROLE_USER'],
        // user identifier key for LDAP
        // this identitfer must be set explicitly
        'uid'
    );
};

Notice 3rd and 4th parameters can be null, because they will never be used: LdapBindAuthenticationProvider will be invoked first, so LDAP connection already will be bound.

The second issue requires a little bit of coding. Symfony has built-in http_basic_ldap authentication provider which perfectly suits your requirements. Unfortunately, Silex doesn't have one, so you need to do it on your own. Use Silex documentation for reference: Defining a custom Authentication Provider

Here is my example of form_login_ldap implementation for Silex. Registering all LDAP-related services:

$app // register other services
    ->register(new LdapServiceProvider())
    ->register(new LdapUsersServiceProvider())
    ->register(new LdapSecurityServiceProvider())
    ->register(new \Silex\Provider\SecurityServiceProvider(), [
        'security.firewalls' => [
            'login' => [
                'pattern' => '^/login$',
            ],
            'secured' => [
                'pattern' => '^.*$',
                'form_login_ldap' => [
                    'login_path' => 'login',
                    'check_path' => 'login_check',
                    'default_target_path' => 'backoffice',
                ],
                'users' => $this['ldap.users'],
            ],
        ],
    ])
;

Service Provider for LDAP Adapter

use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Symfony\Component\Ldap\Ldap;

class LdapServiceProvider implements ServiceProviderInterface
{
    public function register(Container $app)
    {
        $app['ldap'] = function () {
            return Ldap::create('ext_ldap', [
                'connection_string' => 'ldap.example.com',
            ]);
        };
    }
}

Service Provider for LDAP Users

use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Symfony\Component\Security\Core\User\LdapUserProvider;

class LdapUsersServiceProvider implements ServiceProviderInterface
{
    public function register(Container $app)
    {
        $app['ldap.users'] = function () use ($app) {
            return new LdapUserProvider(
                $app['ldap'],
                'dc=example,dc=com',
                null,
                null,
                ['ROLE_USER'],
                'uid'
            );
        };
    }
}

Service Provider for Security Authentication Listener Factory for LDAP Form (the most interesting part for you)

use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider;

class LdapSecurityServiceProvider implements ServiceProviderInterface
{
    public function register(Container $app)
    {
        $app['security.authentication_listener.factory.form_login_ldap'] = $app->protect(function ($name, $options) use ($app) {
            // define the authentication provider object
            $app['security.authentication_provider.'.$name.'.form_login_ldap'] = function () use ($app, $name) {
                return new LdapBindAuthenticationProvider(
                    $app['security.user_provider.'.$name],
                    $app['security.user_checker'],
                    $name,
                    $app['ldap'],
                    'uid={username},dc=example,dc=com',
                    $app['security.hide_user_not_found']
                );
            };

            // define the authentication listener object
            $app['security.authentication_listener.'.$name.'.form_login_ldap'] = $app['security.authentication_listener.form._proto']($name, $options);

            // define the entry point object
            $app[$entryPoint = 'security.entry_point.'.$name.'.form_login_ldap'] = $app['security.entry_point.form._proto']($name, array());

            return array(
                // the authentication provider id
                'security.authentication_provider.'.$name.'.form_login_ldap',
                // the authentication listener id
                'security.authentication_listener.'.$name.'.form_login_ldap',
                // the entry point id
                $entryPoint,
                // the position of the listener in the stack
                'form'
            );
        });
    }
}