Adldap2-Laravel icon indicating copy to clipboard operation
Adldap2-Laravel copied to clipboard

using 2 different AD servers, one as a fallback

Open justageek opened this issue 5 years ago • 15 comments

  • Laravel Version: 6.1
  • Adldap2-Laravel Version: 10.2.3
  • PHP Version:7.2
  • LDAP Type: ActiveDirectory

Description:

We are trying to use 2 ldap servers we are required to maintain, where one is default and one is a fallback if authentication in the default fails. What I have tried is

  • Create a new user provider that extends your DatabaseUserProvider
  • Create my second connection in ldap.php
  • In my user provider, use Resolver::setConnection() in my own ::retrieveByCredentials() and ::validateCredentials() methods to try to use my second ldap connection.
  • If authentication fails, let it fall back to the parent, which is your default DatabaseUserProvider class, which hopefully will use the default connection.

So far this doesn't seem to be working and I wanted to see if you see a flaw in my logic, I cannot tell what is failing right now. I did notice though when I open up the laravel log file, even after authentication fails and the application is not running, the ldap library continues to log connection attempts in the log file, like it is running in the background, trying to connect to both ldap servers in a loop, it looks like it is stuck in some sort of loop.

Any help is appreciated.

Steps To Reproduce:

justageek avatar May 26 '20 21:05 justageek

Hi @justageek, thanks so much for the sponsorship! ❤️

This may be tricky since multi-domain support isn’t built in, but I’ll try to help as much as I can.

I’m tied up for bit of the night, but I’ll write up a response later and we can get try to get this working.

stevebauman avatar May 26 '20 22:05 stevebauman

Hi @justageek, I'm ready to help tackle this with you.

Before you call Resolver::setConnection() I believe you should first attempt connecting to that server -- otherwise the Resolver::byCredentials() method will return null since the LDAP query will fail to run. I believe this may be a step that is missing.

Can you share your code along with where you are doing this logic during authentication?

stevebauman avatar May 27 '20 13:05 stevebauman

@stevebauman , here is my custom user provider and the ldap_auth.php file so you can see how it is used.

AuditPortalUserProvider.php.zip ldap_auth.php.zip

justageek avatar May 27 '20 19:05 justageek

Ok great thanks!

So you have two options:

  1. Set auto_connect to true in your config/ldap.php file for both of your LDAP connections
  2. Attempt connecting to the LDAP connection prior to calling Resolver::byCredentials()

Do you have auto_connect enabled on your connections? This option will attempt to bind to your configured LDAP servers when the Resolver is called -- otherwise you have to connect manually.

Also, if you could post the logs that get generated upon login, that would help me debug with you.

You should only need to call Resolver::setConnection('audit-portal'); once in the providers retrieveByCredentials method.

However, I'm not seeing any kind of fallback logic here -- only one LDAP connection is being used.

I would suggest looping through your configured LDAP connections in the retrieveByCredentials method and setting the Resolver connection like so:

public function retrieveByCredentials(array $credentials)
{
    foreach (config('ldap.connections', []) as $name => $config) {
        Resolver::setConnection($name);
        
        if (!$user = parent::retrieveByCredentials($credentials)) {
            continue;
        }

        return $user;
    }
}

Let me know if that works! 👍

stevebauman avatar May 27 '20 20:05 stevebauman

Thanks again Steve, I have to admit I don't 100% follow the logic, we do have auto_connect set to true for both connections by default. What do I call in order to do the following: Attempt connecting to the LDAP connection prior to calling Resolver::byCredentials() One thing that confused me is since ldap_auth.php has one single key/value for 'connection', it seems like every time you try to authenticate, it is going to try to use that connection value, maybe that doesn't matter. So, the first time it tries to authenticate someone, it will use my custom user provider by try to use the default connection, since that is how ldap_auth.php is set up right now. I will give your loop idea a try and see what happens. Do I need to disable the "fallback" logic so if one authentication fails it doesnt' automatically try to fallback to the next?

justageek avatar May 27 '20 21:05 justageek

Steve, I have a weird mess here, my user is being found in the default ldap server, which is correct for my test, but then fails authentication because it looks like the code moves on to the second connection before it tries the authentication step.

User 'Matthew Pearce' has been successfully found for authentication.
[2020-05-27 21:50:33] local.INFO: User 'Matthew Pearce' is being synchronized.
[2020-05-27 21:50:33] local.INFO: User 'Matthew Pearce' has been successfully synchronized.
[2020-05-27 21:50:33] local.INFO: User 'Matthew Pearce' is authenticating with username:

I don't know why it moves on to the second connection after finding the user in the default ldap server. Maybe because I am setting the connection in ::validateCredentials() method where I should not be doing that? I think that might be the problem.

justageek avatar May 27 '20 21:05 justageek

One thing that confused me is since ldap_auth.php has one single key/value for 'connection', it seems like every time you try to authenticate, it is going to try to use that connection value, maybe that doesn't matter.

Adldap2 doesn't support multi-domain authentication out of the box. This is basically what you're attempting to patch in here. LdapRecord-Laravel (the successor to Adldap2) does however support multi-domain authentication.

Do I need to disable the "fallback" logic so if one authentication fails it doesn't automatically try to fallback to the next?

The fallback feature allows non LDAP users to sign into your Laravel application. For example, if you have a regular user register in your Laravel application, they can continue to sign in without existing in your LDAP server.

Disable it if you do not need / want non LDAP users to sign into your app.

I don't know why it moves on to the second connection after finding the user in the default ldap server. Maybe because I am setting the connection in ::validateCredentials() method where I should not be doing that? I think that might be the problem.

Yup absolutely -- I would remove that from the validateCredentials() method. Once a user is successfully retrieved from your LDAP server via the retrieveByCredentials() method, we want to leave the set connection in place so authentication is done on that connection.

stevebauman avatar May 28 '20 14:05 stevebauman

Steve, thanks for your help , I am really close, I have the authentication working with your loop through the connections, however for some reason if I the login_fallback set to true, it connects and binds to the ldap server but never searches for and locates the user and then proceeds to authentication for some reason, it works if I set it to false. I guess it falls back to fast to the Eloquent login credentials, which my test user doesn't have.

justageek avatar May 28 '20 19:05 justageek

No problem @justageek! Sounds like we're getting close.

however for some reason if I the login_fallback set to true, it connects and binds to the ldap server but never searches for and locates the user and then proceeds to authentication for some reason, it works if I set it to false.

In your application, are you only wanting LDAP users to be able to sign in? Or will your app support registering / creating local users that must be able to sign in as well -- alongside LDAP users?

The login fallback option is for the latter of the above (supporting sign in for local app users who do not exist in your LDAP servers).

If you wish to support this, we will need to override a bit more logic and I will help you with that once I know the direction you're looking to take in your app. Thanks!

stevebauman avatar May 28 '20 20:05 stevebauman

Steve, so I don't know what to think, if I have login_fallback set to true, I cannot login with a user that is in the second Active Directory server. But if I set it to false, the login for the default AD server doesn't work. We do have database users synced up to the default ldap server, so if I have the login_fallback set to false, it isn't authenticated against the default AD server, then it is not falling back to the database, I don't know why the Active Directory isn't finding the user and then authenticating them for the default connection my new loop logic, but it isn't. The logs seem to indicate it is going nuts cycling between the connections

[2020-05-28 20:09:38] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Binding - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:38] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Bound - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:38] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Binding - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:39] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Bound - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:43] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Binding - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:44] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Bound - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:44] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Binding - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:44] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Bound - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:48] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Binding - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:49] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Bound - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:49] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Binding - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:49] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Bound - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:54] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Binding - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:55] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Bound - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:55] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Binding - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:55] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Binding - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:55] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Bound - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:55] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Bound - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:55] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Binding - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:56] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Bound - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:56] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Binding - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:57] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Bound - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:09:57] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Binding - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:57] local.INFO: LDAP (ldaps://cashtn.com:636) - Connection: audit-portal - Operation: Bound - Username: CASHTN\svc_auditor_portal  
[2020-05-28 20:09:59] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Binding - Username: CASHEXPRESSLLC\auditorportal  
[2020-05-28 20:10:00] local.INFO: LDAP (ldaps://dc-cluster.cashexpressllc.com:636) - Connection: default - Operation: Bound - Username: CASHEXPRESSLLC\auditorportal  

justageek avatar May 28 '20 20:05 justageek

Ok -- let me make some patches here. Shouldn't take long. Once I'm done, I'll need you to perform a composer update. I'll post here again shortly!

stevebauman avatar May 28 '20 20:05 stevebauman

ok

justageek avatar May 28 '20 20:05 justageek

Ok -- now I'll need you to update your composer.json file with the following to receive the latest patch:

"adldap2/adldap2-laravel": "dev-master",

Then, run the following to download it:

composer update

Afterwards, in your custom user provider you have, paste in the following code:

/**
 * Whether to fallback to Eloquent authentication.
 *
 * @var bool
 */
protected $fallback = false;

public function retrieveByCredentials(array $credentials)
{
    foreach ($connections = config('ldap.connections', []) as $name => $config) {
        Resolver::setConnection($name);

        if ($user = parent::retrieveByCredentials($credentials)) {
            return $user;
        }

        end($connections);

        // Here we will determine if we are on the last connection. If
        // so, we will enable fallback to Eloquent authentication and
        // attempt to retrieve the local database user to fallback to.
        if ($name == key($connections)) {
            $this->fallback = true;

            return $this->eloquent->retrieveByCredentials($credentials);
        }
    }
}

protected function isFallingBack()
{
    return $this->fallback;
}

Once complete, try authenticating against both domains you have configured, and then a local database user.

Let me know if it works!

stevebauman avatar May 28 '20 20:05 stevebauman

Hi Steve, I need to run a full set of tests, but I think it is going to work. I cannot thank you enough for all the work on this, you are awesome for doing it for us!

justageek avatar May 28 '20 21:05 justageek

Sounds good! And I’m happy to help and I will always support my sponsors 😄

Will wait for your update 👍

stevebauman avatar May 28 '20 22:05 stevebauman