DirectoryTree / LdapRecord-Laravel

Multi-domain LDAP Authentication & Management for Laravel.
https://ldaprecord.com/docs/laravel/v3
MIT License
509 stars 54 forks source link

[Bug] Not able to authenticate users in Jetstream app with ActiveDirectory #458

Closed tal3nce closed 2 years ago

tal3nce commented 2 years ago

Environment:

I can't login following the documentation for Laravel Jetstream (using Laravel 8 because of PHP v7). I believe I followed all the necessary steps, although I admit this is very new to me so I might be missing something obvious. I can establish a connection to the server (via php artisan ldap:test) and I can also validate my credentials using something like $connection->auth()->attempt($user, $pass) however if I use the Auth facade like in the docs, that's where I run into issues since my Auth::validate inside the AuthServiceProvider keeps throwing false when I dd the result. Auth::attempt() also does it. So I end up getting the "These credentials do not match our records." message.

I also tried to put this after my Auth::validate in the AuthServiceProvider and got the following message:

use Ldaprecord\Container;
...

$connection = Container::getConnection('default');
dd($connection->getLdapConnection()->getDiagnosticMessage());

"000004DC: LdapErr: DSID-0C0907E9, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v2580"

Here is my code:

// config/ldap.php
'connections' => [
      'default' => [
              'hosts' => [env('LDAP_TEVACORP_HOST', '127.0.0.1')],
              'username' => env('LDAP_TEVACORP_USERNAME', 'cn=user,dc=local,dc=com'),
              'password' => env('LDAP_TEVACORP_PASSWORD', 'secret'),
              'port' => env('LDAP_TEVACORP_PORT', 389),
              'base_dn' => env('LDAP_TEVACORP_BASE_DN', 'dc=acme,dc=com'),
              'timeout' => env('LDAP_TIMEOUT', 5),
              'use_ssl' => env('LDAP_SSL', false),
              'use_tls' => env('LDAP_TLS', false),
              'options' => [
                  LDAP_OPT_PROTOCOL_VERSION => env('LDAP_OPT_PROTOCOL_VERSION', 3),
                  LDAP_OPT_REFERRALS => env('LDAP_OPT_REFERRALS', 0)
              ]
       ],
]

I don't think I have a clash between .env and "published config file" methods because I don't use LDAP_CONNECTIONS in my .env, I just use the connection-specific env-variables. Note that I'll have to use about 2 dozen different connections, but I first want to make it work with this one.

// config/auth.php

'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

'providers' => [
        'users' => [
            'driver' => 'ldap',
            'model' => LdapRecord\Models\ActiveDirectory\User::class,
            'rules' => [],
            'database' => [
                'model' => App\User::class,
                'sync_passwords' => false,
                'sync_attributes' => [
                    // 'name' => 'cn',
                    'email' => 'mail',
                ],
            ],
        ],
    ],

I'm still trying to figure things out here, but I wouldn't say this might be an issue. I even tried without the database part just to see if "plain auth" would work, but nothing changed for the better.

This one is exactly like in the docs.

// AuthServiceProvider.php

public function boot()
    {
        $this->registerPolicies();

        Fortify::authenticateUsing(function ($request) {
            $validated = Auth::validate([
                'mail' => $request->email,
                'password' => $request->password
            ]);

            // $connection = Container::getConnection('default');
            // dd($message = $connection->getLdapConnection()->getDiagnosticMessage());

            return $validated ? Auth::getLastAttempted() : null;
        });
    }

My default setting for guarding routes is auth:sanctum and I even changed that to auth:web but it didn't help.

I kept my Fortify and Jetstream config files unchanged (I actually did change the 'features' before so it is basically the same as in docs - all features commented out), but I will need to change them once we figure this issue out because I will use the "username" instead of email. Please note that, as said, I could use the $connection()->auth()->attempt() successfully both with domain\username and username@domain so I don't think this has something to do with my AD not allowing authentication via email. I tried switching between file and database session drivers (even changing the UUID type to varchar like it was mentioned somewhere), that didn't work either. I use Laravel v8.83.18 with Jetstream v2.9.0 that has Fortify v1.13.0 (so it's higher than v.1.2.0 that introduced granular authentication methods which gave the possibility to use the method described in the documents via AuthServiceProvider).

Just to note that the app worked perfectly with out of the box settings and a "regular" user in the database, but since this should be run in my company, having authentication against AD is a must.

I tried to be as detailed as possible and I think I have exhausted all possibilities, so I would appreciate your help here. Thanks a lot. Stefan

tal3nce commented 2 years ago

OK, I managed to find out what the reason was. I connected anonymously (username and pass were null in .env). This didn't prevent me from successfully running php artisan ldap:test but it couldn't find in the docs that I had to use username and password (correct me if I'm wrong, if not, maybe this can be added to the docs). This is why I never paid attention to it because my connection was successful from the get go.

My question now is (in case I run into pushback from IT to create a dummy app-specific user for each domain): is there a way to (relatively) elegantly login with anonymous bind? I know I can fetch the user from AD and then store it in the DB manually and create a session, but is there a way to leverage built-in Auth features? Doing it manually is a lot of work and a pity that I can't leverage Laravel's tools for this, not to mention it's less secure since I'd be writing this sensitive logic myself... Any ideas? Thank you!

stevebauman commented 2 years ago

Hi there @tal3nce,

but it couldn't find in the docs that I had to use username and password

A username and password is always shown in the Laravel configuration guide:

https://ldaprecord.com/docs/laravel/v2/configuration#using-a-published-configuration-file

And it's mentioned in the core docs here:

https://ldaprecord.com/docs/core/v2/configuration/#username--password

If you would like to use anonymous binds, you will have to grant permissions to anonymous users to be able to read the directory where your users are located for them to be discovered by your application through LdapRecord.

My question now is (in case I run into pushback from IT to create a dummy app-specific user for each domain): is there a way to (relatively) elegantly login with anonymous bind?

LdapRecord needs permission to search your directory to be able to locate users whom are logging into your application.

Think of it like a database -- you must provide credentials to Laravel to perform operations in the database, and most importantly, perform queries. This is how LdapRecord interacts with your LDAP directory.

Let me know if you have any further questions 👍

stevebauman commented 2 years ago

I know I can fetch the user from AD and then store it in the DB manually and create a session, but is there a way to leverage built-in Auth features? Doing it manually is a lot of work and a pity that I can't leverage Laravel's tools for this,

LdapRecord-Laravel handles this all for you. You shouldn't need to build functionality that creates sessions and synchronize's users. This is provided out-of-the-box.

tal3nce commented 2 years ago

Hi, @stevebauman. Thanks a lot for your feedback. It makes sense what you wrote. It's just that the ldap:test works and then I default to thinking this is all good even though I managed to connect as anonymous user only.

Anyway, I managed to make it work and will have a call with IT to set up a service user for the application.

I have another question on handling this with multiple domains and Jetstream (because of Fortify), but I already saw something similar so I'll comment there when I rest a little bit.

Have a great weekend!