Adldap2 / Adldap2-Laravel

LDAP Authentication & Management for Laravel
MIT License
911 stars 184 forks source link

Can authenticate using tinker but not through frontend #637

Closed matthenning closed 5 years ago

matthenning commented 5 years ago

Description:

After upgrading from 4.* to 5.0 I can only authenticate using tinker but not through the application itself.

$ php7.2 artisan tinker
Psy Shell v0.9.9 (PHP 7.2.12-1+0~20181112102304.11+stretch~1.gbp55f215 — cli) by Justin Hileman
>>> use Adldap\Laravel\Facades\Adldap
>>> $adldap = Adldap::getFacadeRoot()
=> Adldap\Adldap {#3041}
>>> $adldap->connect()
=> Adldap\Connections\Provider {#3037}
>>> $adldap->auth()->attempt('***', '***')
=> true

I have a copy of the application running on the exact same system with the previous version so I don't think this is related to the SELinux issues mentioned in #597. The only difference is a switch to 5.0.2 and composer update. After that I followed the upgrade guide.

Edit: ldap_auth.php:

<?php

return [

    'connection' => env('LDAP_CONNECTION', 'default'),

    'provider' => Adldap\Laravel\Auth\DatabaseUserProvider::class,

    'rules' => [
        Adldap\Laravel\Validation\Rules\DenyTrashed::class,
    ],

    'scopes' => [
        Adldap\Laravel\Scopes\UidScope::class,
    ],

    'usernames' => [
        'ldap' => [
            'discover' => 'uid',
            'authenticate' => 'uid',
        ],
        'eloquent' => 'name',
        'windows' => [
            'discover' => 'uid',
            'key' => 'AUTH_USER',
        ],
    ],

    'passwords' => [
        'sync' => env('LDAP_PASSWORD_SYNC', false),
        'column' => 'password',
    ],

    'login_fallback' => env('LDAP_LOGIN_FALLBACK', false),

    'sync_attributes' => [
        'email' => 'userprincipalname',
        'name' => 'uid',
    ],

    'logging' => [
        'enabled' => true,
        'events' => [
            \Adldap\Laravel\Events\Importing::class => \Adldap\Laravel\Listeners\LogImport::class,
            \Adldap\Laravel\Events\Synchronized::class => \Adldap\Laravel\Listeners\LogSynchronized::class,
            \Adldap\Laravel\Events\Synchronizing::class => \Adldap\Laravel\Listeners\LogSynchronizing::class,
            \Adldap\Laravel\Events\Authenticated::class => \Adldap\Laravel\Listeners\LogAuthenticated::class,
            \Adldap\Laravel\Events\Authenticating::class => \Adldap\Laravel\Listeners\LogAuthentication::class,
            \Adldap\Laravel\Events\AuthenticationFailed::class => \Adldap\Laravel\Listeners\LogAuthenticationFailure::class,
            \Adldap\Laravel\Events\AuthenticationRejected::class => \Adldap\Laravel\Listeners\LogAuthenticationRejection::class,
            \Adldap\Laravel\Events\AuthenticationSuccessful::class => \Adldap\Laravel\Listeners\LogAuthenticationSuccess::class,
            \Adldap\Laravel\Events\DiscoveredWithCredentials::class => \Adldap\Laravel\Listeners\LogDiscovery::class,
            \Adldap\Laravel\Events\AuthenticatedWithWindows::class => \Adldap\Laravel\Listeners\LogWindowsAuth::class,
            \Adldap\Laravel\Events\AuthenticatedModelTrashed::class => \Adldap\Laravel\Listeners\LogTrashedModel::class,
        ],
    ],
];

ldap.php:

<?php

return [
    'connections' => [
        'default' => [
            'auto_connect' => env('LDAP_AUTO_CONNECT', true),

            'connection' => Adldap\Connections\Ldap::class,

            'settings' => [
                'schema' => Adldap\Schemas\OpenLDAP::class,
                'account_prefix' => env('LDAP_ACCOUNT_PREFIX', ''),
                'account_suffix' => env('LDAP_ACCOUNT_SUFFIX', ''),
                'hosts' => explode(' ', env('LDAP_HOSTS')),
                'port' => env('LDAP_PORT', 389),
                'timeout' => env('LDAP_TIMEOUT', 5),
                'base_dn' => env('LDAP_BASE_DN'),
                'username' => env('LDAP_USERNAME'),
                'password' => env('LDAP_PASSWORD'),
                'follow_referrals' => false,
                'use_ssl' => env('LDAP_USE_SSL', false),
                'use_tls' => env('LDAP_USE_TLS', false),
            ],
        ],
    ],
];

.env:

LDAP_HOSTS=ldap.***
LDAP_PORT=636
LDAP_BASE_DN="ou=***,ou=***"
LDAP_USERNAME="uid=***,ou=***"
LDAP_PASSWORD=***
LDAP_USE_TLS=false
LDAP_USE_SSL=true
LDAP_ACCOUNT_PREFIX="uid="
LDAP_ACCOUNT_SUFFIX=",ou=***"
matthenning commented 5 years ago

Weirdly despite being able to authenticate using tinker i can't sync that same user with the artisan command:

$ php7.2 artisan adldap:import ***

   Adldap\Models\ModelNotFoundException  : No LDAP query results for filter: [(&(|(name=***)(mail=***)(uid=***)(sn=***)(givenname=***)(cn=***)(displayname=***))(objectclass=inetorgperson)(objectclass=person)(!(objectclass=contact))(uid=*))] in: [ou=***]

  at /usr/share/ebimonitoring/vendor/adldap2/adldap2/src/Query/Builder.php:572
    568|
    569|         // Make sure we check if the result is an entry or an array before
    570|         // we throw an exception in case the user wants raw results.
    571|         if (!$entry instanceof Model && !is_array($entry)) {
  > 572|             throw (new ModelNotFoundException())
    573|                 ->setQuery($this->getUnescapedQuery(), $this->getDn());
    574|         }
    575|
    576|         return $entry;

  Exception trace:

  1   Adldap\Query\Builder::findOrFail("***")
      /usr/share/ebimonitoring/vendor/adldap2/adldap2-laravel/src/Commands/Console/Import.php:207

  2   Adldap\Laravel\Commands\Console\Import::getUsers()
      /usr/share/ebimonitoring/vendor/adldap2/adldap2-laravel/src/Commands/Console/Import.php:47
matthenning commented 5 years ago

I've narrowed it down to the filter grammar compiler of Adldap2 but I think the root cause comes from the configuration.

The Connections\Ldap search method seems to return a broken filter. The returned filter looks like this: [(&(|(name=myuser)(mail=myuser)(uid=myuser)(sn=myuser)(givenname=myuser)(cn=myuser)(displayname=myuser))(objectclass=inetorgperson)(objectclass=person)(!(objectclass=contact))(uid=*))] in: [ou=***]

When I overwrite the $filter with uid=myuser as I would've expected from the configuration above I get a result:

public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0)
{
    $filter = 'uid=myuser';
    return ldap_search($this->getConnection(), $dn, $filter, $fields, $onlyAttributes, $size, $time);
}

The login through the application is working fine now, too. Maybe there's something wrong with my configuration leading to an invalid filter string, but I haven't found it yet.

stevebauman commented 5 years ago

Hi @matthenning,

Weirdly despite being able to authenticate using tinker i can't sync that same user with the artisan command:

When you enter an value with the php artisan adldap:import command (such as php artisan adldap:import jdoe), ANR is used (when using ActiveDirectory). However, since you're using OpenLDAP, an ANR equivalent query is built to search for the first user with one of the following attributes:

name
mail
uid
sn
givenname
cn
displayname

See:

https://github.com/Adldap2/Adldap2/blob/e7376c1aaed0bcfb6e41558a816ffe1c9ff0b82a/src/Query/Builder.php#L507-L531

The filter is actually valid:

(&
    (|
        (name=myuser)
        (mail=myuser)
        (uid=myuser)
        (sn=myuser)
        (givenname=myuser)
        (cn=myuser)
        (displayname=myuser)
    )
    (objectclass=inetorgperson)
    (objectclass=person)
    (!
        (objectclass=contact)
    )
    (uid=*)
)

And you would actually receive a PHP warning for a bad search filter:

Warning: ldap_search(): Search: Bad search filter

You mention you login is now working on your application, but your tinker example is using Adldap::auth()->attempt() which is simply calling an ldap_bind against your configured LDAP server, but calling Laravel's Auth::attempt() method would actually authenticate you in your application and use the Adldap2-Laravel auth driver.

Looking at your ENV, you've set a base DN with some OU's inside (LDAP_BASE_DN="ou=***,ou=***"). Is the user you're trying to authenticate against contained inside this OU?

matthenning commented 5 years ago

To clarify: Only when modifying the filter I can login through the frontend. I tried this because the filter returned by the library seems impossible to match as it's looking for objectclass=inetorgperson AND objectclass=person. I'm not sure whether a person equals an inetorgperson as I'm no LDAP expert, it just looked fishy.

The bind user is located in the OU above the users like uid=binduser,ou=department,o=*,c=* and the users are uid=myuser,ou=users,ou=department,o=*,c=*

stevebauman commented 5 years ago

Hi @matthenning,

I'm not sure whether a person equals an inetorgperson as I'm no LDAP expert, it just looked fishy.

The objectClass attribute is multi-valued, so it should contain both inetorgperson and person, but I could definitely be mistaken for OpenLDAP. Are you able to browse the user attributes on the LDAP record and see what the user has for their objectClass?

Also, can you try removing the default scope Adldap\Laravel\Scopes\UidScope::class from your ldap_auth.php configuration, then run php artisan config:clear and try authenticating again?

matthenning commented 5 years ago

The objectClass is inetOrgPerson.

Removing the scope didn't do the trick. Weirdly enough after upgrading I don't get any log output from Adldap2 despite having it enabled in ldap_auth.php:

'logging' => [

        'enabled' => true,

        'events' => [

            \Adldap\Laravel\Events\Importing::class => \Adldap\Laravel\Listeners\LogImport::class,
            \Adldap\Laravel\Events\Synchronized::class => \Adldap\Laravel\Listeners\LogSynchronized::class,
            \Adldap\Laravel\Events\Synchronizing::class => \Adldap\Laravel\Listeners\LogSynchronizing::class,
            \Adldap\Laravel\Events\Authenticated::class => \Adldap\Laravel\Listeners\LogAuthenticated::class,
            \Adldap\Laravel\Events\Authenticating::class => \Adldap\Laravel\Listeners\LogAuthentication::class,
            \Adldap\Laravel\Events\AuthenticationFailed::class => \Adldap\Laravel\Listeners\LogAuthenticationFailure::class,
            \Adldap\Laravel\Events\AuthenticationRejected::class => \Adldap\Laravel\Listeners\LogAuthenticationRejection::class,
            \Adldap\Laravel\Events\AuthenticationSuccessful::class => \Adldap\Laravel\Listeners\LogAuthenticationSuccess::class,
            \Adldap\Laravel\Events\DiscoveredWithCredentials::class => \Adldap\Laravel\Listeners\LogDiscovery::class,
            \Adldap\Laravel\Events\AuthenticatedWithWindows::class => \Adldap\Laravel\Listeners\LogWindowsAuth::class,
            \Adldap\Laravel\Events\AuthenticatedModelTrashed::class => \Adldap\Laravel\Listeners\LogTrashedModel::class,

        ],
    ],

I'm flying blind right now, which hinders debugging this further.

stevebauman commented 5 years ago

Did you clear your config cache after upgrading using php artisan config:clear?

matthenning commented 5 years ago

I added a dd($query); to Query\Grammar.php+56 and got the following result (username redacted):

(&(objectclass=\69\6e\65\74\6f\72\67\70\65\72\73\6f\6e)(objectclass=\70\65\72\73\6f\6e)(!(objectclass=\63\6f\6e\74\61\63\74))(uid=*)(uid=redacted))

When translating the hex this results in:

(&(objectclass=inetorgperson)(objectclass=person)(!(objectclass=contact))(uid=*)(uid=redacted))

When I cut this down by removing the objectclass contact filter I can successfully authenticate:

(&(objectclass=inetorgperson)(objectclass=person)(uid=*)(uid=redacted))

Could it be that my object class is not only inetorgperson and person but also contact?

Edit: Yes I cleared the config.

stevebauman commented 5 years ago

Ok, thanks @matthenning, appreciate all the debugging you've been going through. There's definitely an issue with the query for OpenLDAP users. I'm going to install a server on my machine today and do some debugging as well.

Are you able to dump your users attributes and see if it contains the objectclass of contact? I'd be surprised if that was the case, but I don't have much experience with OpenLDAP to be honest, so I could definitely be wrong.

stevebauman commented 5 years ago

Related https://github.com/Adldap2/Adldap2/pull/612

matthenning commented 5 years ago

Weird, it doesn't contain contact:

"objectclass": [
    "person",
    "top",
    "inetOrgPerson",
    "organizationalPerson"
]

I double checked that removing (!(objectclass=contact)) fixes the problem though.

stevebauman commented 5 years ago

So strange... Okay, I'll put a patch out for this shortly. Thanks @matthenning!

stevebauman commented 5 years ago

Fixed in v9.1.4. Run a composer update and you should be all set.

Thanks again!