Testing
Introduction
LdapRecord-Laravel prides itself on giving you a great and easy testing experience using the Directory Emulator. Using it, we can test authentication rules, scopes, and group memberships.
Getting Started
Before we begin, you must require the doctrine/dbal
into your composers require-dev
for testing.
This is due to the $table->dropColumns(['guid', 'domain'])
call inside the additional
LdapRecord auth migration and that we are using SQLite in our test environment.
This package is required for modifying columns - as described in the Laravel documentation.
To do so, run the following command:
composer require doctrine/dbal --dev
Creating the test
Let's whip up a test by running the following command:
php artisan make:test LdapAuthenticationTest
Inside our generated test, we'll make use of the following traits:
DatabaseMigrations
Illuminate\Foundation\Testing\DatabaseMigrations
Using this trait will execute our migrations and ensure our database is ready to import our LDAP user.
WithFaker
Illuminate\Foundation\Testing\WithFaker
Using this trait provides us with generating fake UUID's (great for creating mock "guids"), names and emails.
Let's add a test_auth_works
method into the generated test:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Auth;
use LdapRecord\Laravel\Testing\DirectoryEmulator;
use LdapRecord\Models\ActiveDirectory\User;
use Tests\TestCase;
class LdapAuthenticationTest extends TestCase
{
use DatabaseMigrations, WithFaker;
public function test_auth_works()
{
$fake = DirectoryEmulator::setup('default');
$ldapUser = User::create([
'cn' => $this->faker->name,
'mail' => $this->faker->email,
'objectguid' => $this->faker->uuid,
]);
$fake->actingAs($ldapUser);
$this->post('/login', [
'email' => $ldapUser->mail[0],
'password' => 'secret',
])->assertRedirect('/home');
$user = Auth::user();
$this->assertInstanceOf(\App\Models\User::class, $user);
$this->assertEquals($ldapUser->mail[0], $user->email);
$this->assertEquals($ldapUser->cn[0], $user->name);
}
}
Let's deconstruct what's going on here step by step.
$fake = DirectoryEmulator::setup('default');
This first line creates a new Directory Emulator for our LDAP connection named default
inside
of our config/ldap.php
file. It returns a fake LDAP connection that we can use to indicate
that the user we create in this fake directory will successfully pass LDAP authentication.
$user = User::create([
'cn' => $this->faker->name,
'mail' => $this->faker->email,
'objectguid' => $this->faker->uuid,
]);
On the second line, we're creating our fake LDAP user who will be signing in to our application.
You'll notice that we assign the attributes that are inside our sync_attributes
specified inside our config/auth.php
file, as well as the users objectguid
.
If you're using OpenLDAP, the
objectguid
field may beentryUUID
oruid
.
This is a good place to test attribute synchronization.
$fake->actingAs($user);
This third line, we are asserting that the user we have created will automatically pass LDAP authentication. If we remove this line, attempting to authenticate as the user will fail, as they are not allowed to bind using your fake connection.
$this->post('/login', [
'email' => $user->mail[0],
'password' => 'secret',
])->assertRedirect('/home');
Fourth, we are sending a post request to our login
page, with our LDAP users email address.
The password can be anything, since we asserted above (using the actingAs()
method) that
the user will pass, regardless of what password we use.
If your application has password synchronization enabled, this is a good place to send various passwords and assert that the hashes match after a successful login.
$user = Auth::user();
$this->assertInstanceOf(\App\Models\User::class, $user);
$this->assertEquals($ldapUser->mail[0], $user->email);
$this->assertEquals($ldapUser->cn[0], $user->name);
Finally, we'll check to make sure we can retrieve the successfully authenticated user and that their attributes were successfully synchronized into our Eloquent database model.
Scopes
To test scopes that you apply to the LdapRecord model you are using for authentication, you will need to apply the attributes to the fake user you create to test that they can be properly located during authentication.
For example, if you created a scope that enforces users to be inside an Organizational Unit, then we must create our fake user inside that Organizational Unit for the user to be located - as you would using a real LDAP directory. Let's walk through this.
Below we have our scope that will enforce users to be located
inside an Organizational Unit named Administrators
:
namespace App\Ldap\Scopes;
use LdapRecord\Models\Model;
use LdapRecord\Models\Scope;
use LdapRecord\Query\Model\Builder;
use LdapRecord\Models\ActiveDirectory\OrganizationalUnit;
class AdministratorsScope implements Scope
{
public function apply(Builder $query, Model $model)
{
$ou = OrganizationalUnit::where('ou', '=', 'Accounting')->first();
$query->in($ou);
}
}
And we have also added it into our model:
namespace App\Ldap;
use LdapRecord\Models\Model;
use App\Ldap\Scopes\AdministratorsScope;
class User extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new AdministratorsScope());
}
}
Now let's create our test. To do so, we'll set up everything as we have in the above test
example, but we will create our user inside the Administrators
Organizational Unit:
public function test_auth_works()
{
$fake = DirectoryEmulator::setup('default');
$ou = OrganizationalUnit::create(['ou' => 'Administrators']);
$ldapUser = (new User)->inside($ou);
$ldapUser->save([
'mail' => $this->faker->email,
'cn' => $this->faker->name,
'objectguid' => $this->faker->uuid,
]);
$fake->actingAs($ldapUser);
$this->post('/login', [
'email' => $ldapUser->mail[0],
'password' => 'secret',
])->assertRedirect('/home');
$user = Auth::user();
$this->assertInstanceOf(\App\Models\User::class, $user);
$this->assertEquals($ldapUser->mail[0], $user->email);
$this->assertEquals($ldapUser->cn[0], $user->name);
}
To test the opposite of the above - such as a user who is not located inside the Administrators
OU, simply create them inside a different OU, or inside the root of your emulated directory:
public function test_auth_fails()
{
$fake = DirectoryEmulator::setup('default');
$ldapUser = User::create([
'cn' => $this->faker->name,
'mail' => $this->faker->email,
'objectguid' => $this->faker->uuid,
]);
$fake->actingAs($ldapUser);
$this->post('/login', [
'email' => $ldapUser->mail[0],
'password' => 'secret',
])->assertSessionHasErrors('email');
$this->assertFalse(Auth::check());
}
Even though we have asserted that the user passes LDAP authentication ($fake->actingAs($ldapUser)
),
authentication will fail due to the user not being able to be located due to our scope we have
created.
We have also modified our redirect assertion to instead validate that the email
session
key contains errors. This key will contain the Invalid credentials
message.
Rules
As with testing scopes, to test rules we must either apply or omit data on our fake user to test our LDAP authentication rules.
An authentication rule is great for checking if a user is a member of a certain group before allowing them to authenticate. Let's walk through an example and test this.
Our application requires the user to be a member of a group called Help Desk
.
With that requirement, here is our created authentication rule:
<?php
namespace App\Ldap\Rules;
use LdapRecord\Laravel\Auth\Rule;
use LdapRecord\Models\Model as LdapRecord;
use LdapRecord\Models\ActiveDirectory\Group;
use Illuminate\Database\Eloquent\Model as Eloquent;
class HelpDeskEmployee implements Rule
{
public function passes(LdapRecord $user, Eloquent $model = null): bool
{
$group = Group::where('name', '=', 'Help Desk')->first();
return $this->user->groups()->exists($group);
}
}
This rule has also been added into our providers configuration inside our config/auth.php
file:
// ...
'providers' => [
// ...
'users' => [
// ...
'rules' => [
\App\Ldap\Rules\HelpDeskEmployee::class,
],
],
]
Now we can create our test to ensure only users who are members of the group can authenticate:
public function test_auth_works()
{
$fake = DirectoryEmulator::setup('default');
$ldapGroup = Group::create(['cn' => 'Help Desk']);
$ldapUser = User::create([
'cn' => $this->faker->name,
'mail' => $this->faker->email,
'objectguid' => $this->faker->uuid,
'memberof' => [$ldapGroup->getDn()],
]);
$ldapGroup->members()->attach($ldapUser);
$fake->actingAs($ldapUser);
$this->post('/login', [
'email' => $ldapUser->mail[0],
'password' => 'secret',
])->assertRedirect('/home');
$user = Auth::user();
$this->assertInstanceOf(\App\Models\User::class, $user);
$this->assertEquals($ldapUser->mail[0], $user->email);
$this->assertEquals($ldapUser->cn[0], $user->name);
}
As you can see above, we created a Help Desk
group, added the group into the users memberof
attribute (due to this field being virtual) and have attached them to the group.
Now let's create a test to ensure users who are not members of the group can't authenticate.
public function test_auth_fails()
{
$fake = DirectoryEmulator::setup('default');
$ldapUser = User::create([
'cn' => $this->faker->name,
'mail' => $this->faker->email,
'objectguid' => $this->faker->uuid,
]);
$fake->actingAs($ldapUser);
$this->post('/login', [
'email' => $ldapUser->mail[0],
'password' => 'secret',
])->assertSessionHasErrors('email');
$this->assertFalse(Auth::check());
}
The above test passes because we have not added our LDAP user into any groups -
so the exists()
check inside our rule returns false
.
SSO / Windows Authentication
To test Sigle-Sign-On (or Windows Authentication) for your Laravel application, you must set the authenticating users down-level logon name as a server variable.
This server variable (typically $_SERVER['AUTH_USER']
) is what the WindowsAuthenticate
middleware reads to locate the authenticated user from your LDAP directory.
To set server variables for upcoming requests inside your Laravel tests, use the withServerVariables()
method:
public function test_windows_authentication_works()
{
DirectoryEmulator::setup('default');
$ldapUser = User::create([
'cn' => $this->faker->name,
'mail' => $this->faker->email,
'objectguid' => $this->faker->uuid,
'samaccountname' => $this->faker->userName,
]);
// Replace 'DOMAIN' with your domain from your configured LDAP
// `base_dn`. For example, if your `base_dn` is equal to
// 'dc=company,dc=com', then you would use 'COMPANY'.
$authUser = implode('\\', [
'DOMAIN', $ldapUser->getFirstAttribute('samaccountname')
]);
// Set the server variables for the upcoming request.
$this->withServerVariables([
WindowsAuthenticate::$serverKey => $authUser
]);
// Attempt accessing a protected page:
$this->get('/dashboard')->assertOk();
// Ensure the user was authenticated:
$this->assertTrue(Auth::check());
}