Ldaprecord-laravel: [Support] Authenticate / Authorize against different LDAP

Created on 4 Aug 2020  路  16Comments  路  Source: DirectoryTree/LdapRecord-Laravel

Hello Steve,

first of all, thank you for that great tool! It works like a charm!
I tested LdapRecord on a fresh ApacheDS by using authentication and authorization. This worked just perfectly.

My next challenge, is to authenticate against LDAP 1 (by connecting to this ldap, with the user credentials provided by the user) and authorize against LDAP 2 (take the authenticated user and synch it with the database.. using a privileged account to search the ldap).

Is there a way to achieve this out of the box?

Right now, I can authenticate the user manually by using the auth()->attempt() method. But I don't know where to put this piece of code to get it to work with the synchronisation of the user attributes by LDAP 2.

If you need more informations, please don't mind to ask.

Thank you in advance.

question

All 16 comments

Hi @gnewmann, thanks so much for the sponsorship! :heart: I really appreciate it.

I tested LdapRecord on a fresh ApacheDS by using authentication and authorization. This worked just perfectly.

I'm glad you were able to get your application up and running!

My next challenge, is to authenticate against LDAP 1 (by connecting to this ldap, with the user credentials provided by the user) and authorize against LDAP 2 (take the authenticated user and synch it with the database.. using a privileged account to search the ldap).

From what I understand, you need to:

  1. Save the credentials of the user who has successfully logged into your application using your LDAP 1 connection
  2. Use those saved credentials for connectivity to your LDAP 2 connection
  3. Connect to your LDAP 2 connection (with the users LDAP 1 credentials) and perform database synchronization using that connection

Am I understanding that correctly?

Hi @stevebauman, you're welcome. Thank you for the quick response :+1:

I would appreciate something like that:

  1. Check the credentials of the user against LDAP 1 (by logging into LDAP 1 with the credentials the user provided in the login form)
    On successful connection to LDAP 1:
  2. Connect to LDAP 2 (using credentials provided in the .env file) and sychronize the data for the given user.

If it would be possible to save the encrypted password in step 1 or 2 it would be a plus.
As I understood the ldap connection will be established over and over again if I don't save the password, am I right?

Thank you so much!

Happy to help @gnewmann! It seems we may be in different time zones (I'm in EST), otherwise I would be able to reply much faster lol.

I'm starting to understand further, though I have a couple more questions to help you write an implementation:

  1. Are these two LDAP connections completely different domains?
  2. Do they have unique Base Distinguished Names (i.e. dc=alpha,dc=com & dc=bravo,dc=com)?
  3. If there are unique domains, are they joined in a trust?

Thanks @stevebaumann.
That's right, I'm in Germany Stuttgart I'm in (CEST +2) :-) At all your replies are pretty quick :+1:

  1. The LDAPS are in the same domain
  2. They have the same base distinguished names (dc=alpha, dc=com)

As far as I understand, LDAP 1 is an ldap (copy of the original) for authentication purposes.
LDAP 2 ist an ldap (copy of the original) providing user data to the applications.

Sadly I cannot tell why this is implemented the way it is.. I got access to the both of them to implement authentication and authoriziation in my application. :/

Thank you so much

Okay great! I fully understand now.

This may be a bit difficult, since this is how LdapRecord-Laravel authenticates users:

  1. User submits login form
  2. LdapRecord-Laravel attempts to locate the user inside of your LDAP directory with the email address / username
    (this is done by "pre-binding" to your LDAP server using the configured credentials in your config/ldap.php file)
  3. Submits their distinguished name and password to the LDAP server for authentication
  4. Imports the users data and then logs them into your Laravel application

It doesn't perform step 3 first -- however, I think we can make this happen if need be.

Have you tested user authentication against LDAP 2? You will need to authenticate against it anyway, so it seems to me that authenticating against LDAP 1 is pointless and doesn't offer any utility. I may be wrong?

Hi @stevebauman

edit: please read next post, the information provided in this post could be useless.

so I checked if I can authenticate agains LDAP 1 using "no" credentials (in the Connection) and try attempt authenticating (the submitted login data from the user) by:
$connection->auth()->attempt()

This worked fine.

I don't mind the sequence, we could first get the data (Step 2) and authenticate afterwards (Step 3).
[of a security point of view, I just thought it could be a bit better to first authenticate and afterwards get some data from the ldap]

So it could be like (added my comments by using '-' )

  1. User submits login form
  2. LdapRecord-Laravel attempts to locate the user inside of your LDAP directory with the email address / username
    (this is done by "pre-binding" to your LDAP server using the configured credentials in your config/ldap.php file)
  3. this step works by using LDAP 2
  4. Submits their distinguished name and password to the LDAP server for authentication
  5. we could use $connection->auth()->attempt() to check the submitted user credentials.
  6. we can connect to the ldap (in general) by using the credentails provided in the .env
  7. Imports the users data and then logs them into your Laravel application

I checked the authentication against LDAP 2 .. I cannot authenticate against it with my user.. because this LDAP doesn't has passwords saved. I only have one given user, to connect to this ldap to get the informations needed.

Thank you!

EDIT:

I talked to our LDAP- Pro.. and he answered me, that there's a way to use LDAP 1 for all authentication and user informaion.
In this case.. the user logins into LDAP 1 with his credentials, for example:
uid=userid,ou=people,o=organisation,c=de and password, using the base DN: uid=userid,ou=people,o=organisation,c=de, then the user gets (only his own) attributes back.

Maybe the implementation is easier on this approach.

I tried it right now and it worked:

    $authenticated = false;

    $connection = new Connection([
            'hosts' => ['ldap'],
            'username' => 'uid=test,ou=people,o=organization,c=de',
            'password' => 'mypassword',
            'port' => 123456,
            'base_dn' => 'uid=test,ou=people,o=organization,c=de',
            'timeout' => 5,
            'use_ssl' => false,
            'use_tls' => true,
            'version'          => 3,
        ]);



        $result= $connection->connect();

    // this works!
        if ($connection->auth()->attempt('uid=test,ou=people,o=organization,c=de', 'mypassword')) {
            $authenticated = true;
        }

    // this works,too
        $results = $connection->query()->get();

Can u please help me to apply this approach to LdapRecord ?

This means:

  • Using the provided username (in the format: "uid=%providedUserId%,ou=people,o=organization,c=de") and password to connect to the ldap-server
  • Using a variable baseDN with format: uid=%providedUserId%,ou=people,o=organization,c=de', 'mypassword'

$providedUserId = the user id provided by the user in the login form.

Thanks Greg

Ok, thanks for following up @gnewmann.

In this case.. the user logins into LDAP 1 with his credentials, for example:
uid=userid,ou=people,o=organisation,c=de and password, using the base DN: uid=userid,ou=people,o=organisation,c=de, then the user gets (only his own) attributes back.

So you're users will be logging in with their full distinguished name? Or their uid?

I believe you could achieve what you're looking for by overriding the guard() method in your LoginController and using the following code:

// app/Http/Controllers/Auth/LoginController.php

/**
 * Get the guard to be used during authentication.
 *
 * @return \Illuminate\Contracts\Auth\StatefulGuard
 */
protected function guard()
{
    $guard = Auth::guard();

    /** @var DatabaseUserProvider $provider */
    $provider = $guard->getProvider();

    // We will override user resolution to attempt to
    // first pre-bind to our LDAP server using the
    // users submitted login credentials.
    $provider->resolveUsersUsing(function ($credentials) use ($provider) {
        $users = $provider->getLdapUserRepository();

        $connection = $users->createModel()->getConnection();

        // Here we will assemble the users distinguished name to
        // be able to bind to our LDAP directory and allow the
        // user to query their own data from the server.
        $username = sprintf('uid=%s,ou=people,o=organisation,c=de', $credentials['username']);

        if (! $connection->auth()->attempt($username, $credentials['password'], $bindAsUser = true)) {
            return;
        }

        // Find the user by distinguished name.
        return $users->query()->find($username);
    });

    $provider->getLdapUserAuthenticator()->authenticateUsing(function () {
        // Since we have already bound using the users
        // credentials, we don't need to re-attempt
        // authentication to our LDAP server.
        return true;
    });

    return $guard;
}

Hello @stevebauman,

thank you for the quick response.

The auth()->attempt(...) worked finde. But i get a problem on: return $users->findByCredentials($credentials).
This method check for a query result on:
if (! is_null($user = $query->first())) {
Sadly, I don't get a result here.

In my code I just enhanced the base_dn (which is the same as the $username), we need this base_dn to get the attributes of the logged in user:
$connection->setConfiguration(['base_dn' => $username]);

If i make a search query in Apache Directory Studio, I also need the search base: uid=myusername,ou=people,o=organisation,c=de, otherwise I get no results.

Thank you

Ah yes you're right! If the user doesn't have permission to search the directory, then you would have to perform a $users->query()->find($username) instead. Please take a look at my updated code example above.

Can you give that a shot and let me know if it works? You shouldn't need to modify your base_dn of the connection. The find() will perform an ldap_read on the given distinguished name and should locate them in your directory.

Let me know! 馃憤

Hey @stevebauman,
thanks for the quick response. I tried to use the find($username), but it didn't work for me. The user has a permission to search the directory.. I tried this in ApacheDS. But as far as I can see, LdapRecord always uses the unmodified credentials (username: test01 instead of uid=test01,ou=people,o=organisation,c=de) and fails on authentication.
As far as I could also see, the function find(..) uses a filter, as:
(&(objectclass=top)(objectclass=person)(objectclass=organizationalperson)(objectclass=inetorgperson)(objectclass=*))

This could make a problem, too.. because the object isn't an organizationalperson and not an inetorgperson.

Thank you in advance.

Greg

But as far as I can see, LdapRecord always uses the unmodified credentials (username: test01 instead of uid=test01,ou=people,o=organisation,c=de) and fails on authentication.

How do you know this? Are you able to reproduce this easily?

As far as I could also see, the function find(..) uses a filter, as:
(&(objectclass=top)(objectclass=person)(objectclass=organizationalperson)(objectclass=inetorgperson)(objectclass=*))

Which LdapRecord model have you configured inside of your config/auth.php file?

Since you're using ApacheDS, you will need to create your own model and add set the object classes that it belongs to.

For example, run:

php artisan make:ldap-model User

Then inside the generated app/Ldap/User.php:

namespace App\Ldap;

use LdapRecord\Models\Model;

class User extends Model
{
    /**
     * The object classes of the LDAP model.
     *
     * @var array
     */
    public static $objectClasses = [
        // Insert each object class ApacheDS users have.
    ];
}

Then, insert your new model inside of your authentication guard (inside config/auth.php):

'providers' => [
    // ...

    'ldap' => [
        'driver' => 'ldap',
        'model' => App\Ldap\User::class, // <-- Inserted here.
        'database' => [
            'model' => App\User::class,
            'sync_passwords' => false,
            'sync_attributes' => [
                'name' => 'cn',
                'email' => 'mail',
            ],
        ],
    ],
],

If you do not do the above, users will not be able to be located properly in your directory. All searches will return no results.

Hi Steve,

How do you know this? Are you able to reproduce this easily?

Yeah, while debugging I could see that the uid just contained the username (test01 instead of uid=test01,ou=people,o=organisation,c=de).

I could fix this by changing the credentials method:

protected function credentials(Request $request)
    {
        return [
             // old: 'uid' => $request->get('username'), 
            'uid' => sprintf('uid=%s,ou=people,o=organisation,c=de',$request->get('username')), // changed
            'password' => $request->get('password'),
        ];
    }

I used the OpenLDAP Model: LdapRecordModelsOpenLdapUser::class. Now it makes sense to me.. this worked on my local LDAP- Server.. but not with the productive LDAP Server. So i generated a LDAP User class, added a guidKey and the objectClasses:


namespace App\Ldap;
use LdapRecord\Models\Model;

class User extends Model
{
    protected $guidKey = 'entryUUID';

    /**
     * The object classes of the LDAP model.
     *
     * @var array
     */
    public static $objectClasses = [
        'top',
        'person'
    ];

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);

    }
}


This did the trick for me!

Is that a solution that you would recommend?

Thank you for the help.

Yeah, while debugging I could see that the uid just contained the username (test01 instead of uid=test01,ou=people,o=organisation,c=de).

Yes it would only contain that, since we assemble the authenticating users distinguished name inside of the guard() method instead. Did you implement the overridden guard() method that I posted above? Wondering if that's working for you or not.

Is that a solution that you would recommend?

Yes absolutely. You have to create a separate model for each LDAP server type you have and are connecting to (such as ApacheDS, OpenLDAP, or Active Directory). LdapRecord doesn't know which type of LDAP server you are connecting to -- you have to specify it in this manor.

If you have a different test LDAP server than production (such as a local ApacheDS server and are using Active Directory in production) then I'd suggest using your applications environment to swap the models, maybe inside of your AppServiceProvider:

public function boot()
{
    if (app()->isLocal()) {
        config('auth.providers.ldap.model', \App\Ldap\ApacheDS\User::class);
    }
}

Hope this helps!

Hi @stevebauman

Yes it would only contain that, since we assemble the authenticating users distinguished name inside of the guard() method instead. Did you implement the overridden guard() method that I posted above? Wondering if that's working for you or not.

Yeah, I used the overridden guard() method.. and it works fine.. I tried it without the overriden method but this doesn't work. I needed to change the credentials function (see my last post), too.

Do u got me some advise: How can I separate production and test on the auth method? (this is more of a laravel question).

Thank you

Hi @gnewmann! Sorry I missed your last comment.

Do u got me some advise: How can I separate production and test on the auth method? (this is more of a laravel question).

You could add an LDAP_DN_FORMAT environment variable for this (if that is all you need to switch), for example:

Inside your .env file:

LDAP_DN_FORMAT="uid=%s,ou=people,o=organisation,c=de"

Inside your LoginController.php:

$username = sprintf(env('LDAP_DN_FORMAT'), $credentials['username']);

Is that what you are referring to by chance? Let me know and I'd be happy to assist further.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siegherr picture siegherr  路  6Comments

kruiz122893 picture kruiz122893  路  5Comments

HepplerDotNet picture HepplerDotNet  路  7Comments

gerardorourke picture gerardorourke  路  6Comments

aanderse picture aanderse  路  3Comments