Laravel Jetstream

Introduction

Laravel Jetstream utilizes Laravel Fortify for authentication under the hood. We will customize various aspects of it to allow our LDAP users to sign in successfully.

Fortify Setup

Authentication Callback

For this example application, we will authenticate our LDAP users with their email address using the LDAP attribute mail.

For LdapRecord to properly locate the user in your directory during sign in, we will override Fortify's authentication callback using the Fortify::authenticateUsing() method in our AuthServiceProvider.php file:

// app/Providers/AuthServiceProvider.php

// ...
use Laravel\Fortify\Fortify;

class AuthServiceProvider extends ServiceProvider
{
    // ...

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

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

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

As you can see above, we set the mail key which is passed to the LdapRecord authentication provider.

A search query will be executed on your directory for a user that contains the mail attribute equal to the entered email that the user has submitted on your login form. The password key will not be used in the search.

If a user cannot be located in your directory, or they fail authentication, they will be redirected to the login page normally with the "Invalid credentials" error message.

You may also add extra key => value pairs in the credentials array to further scope the LDAP query. The password key is automatically ignored by LdapRecord.

Feature Configuration

Since we are synchronizing data from our LDAP server, we must disable the following features by commenting them out inside of the config/fortify.php file:

// config/fortify.php

// Before:
'features' => [
    Features::registration(),
    Features::resetPasswords(),
    // Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    // Features::twoFactorAuthentication(),
],

// After:
'features' => [
    // Features::registration(),
    // Features::resetPasswords(),
    // Features::emailVerification(),
    // Features::updateProfileInformation(),
    // Features::updatePasswords(),
    // Features::twoFactorAuthentication(),
],

Important: You may keep Features::registration() enabled if you would like to continue accepting local application user registration. Keep in mind, if you continue to allow registration, you will need to either use multiple Laravel authentication guards, or setup the login fallback feature.

Using Usernames

To authenticate your users by their username we must adjust some scaffolded code generated by Laravel Jetstream.

In the following example, we will authenticate users by their sAMAccountName.

Fortify Setup

Authentication Callback

With our Fortiy configuration updated, we will jump into our AuthServiceProvider.php file and setup our authentication callback using the Fortify::authenticateUsing() method:

// app/Providers/AuthServiceProvider.php

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

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

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

Username Configuration

Inside of our config/fortify.php file, we must change the username option to username from email:

// config/fortify.php

// Before:
'username' => 'email',

// After:
'username' => 'username',

You will notice above that we are passing in an array of credentials with samaccountname as the key, and the requests username form input.

Database Migration

The built in users database table migration must also be modified to use a username column instead of email:

// database/migrations/2014_10_12_000000_create_users_table.php

// Before:
$table->string('email')->unique();

// After:
$table->string('username')->unique();

Sync Attributes

When using usernames, we must also adjust the sync_attributes option inside of our config/auth.php file. We will adjust it to reflect our username database column to be synchronized with the samaccountname attribute:

// config/auth.php

'providers' => [
    // ...

    'ldap' => [
        // ...
        'database' => [
            // ...
            'sync_attributes' => [
                'name' => 'cn',
                'username' => 'samaccountname',
            ],
        ],
    ],
],

Remember to add any additional database columns you need synchronized here.

Login View

Now we must open up the login.blade.php view and swap the current HTML input field from email to username so we can retrieve it properly in our Fortify::authenticateUsing() callback:

resources
views
auth
login.blade.php
<!-- Before: -->
<div>
    <x-jet-label value="Email" />
    <x-jet-input class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
</div>

<!-- After: -->
<div>
    <x-jet-label value="Username" />
    <x-jet-input class="block mt-1 w-full" type="text" name="username" :value="old('username')" required autofocus />
</div>

User Model

If you plan on allowing non-LDAP users to register and login to your application, you must adjust the $fillable attributes property on your app/Models/User.php to include the username column instead of email:

// app/Models/User.php

class User extends Authenticatable implements LdapAuthenticatable
{
    // ...

    // Before:
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    // After:
    protected $fillable = [
        'name',
        'username',
        'password',
    ];
}

Fallback Authentication

Database fallback allows the authentication of local database users if LDAP connectivity is not present, or an LDAP user cannot be found.

To enable this feature, you must define a fallback array inside of the credentials you insert into the Auth::validate() method in your Fortify::authenticateUsing() callback:

// app/Providers/AuthServiceProvider.php

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

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

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

For example, given the following users database table:

id name email password guid domain
1 Steve Bauman sbauman@outlook.com ... null null

If a user attempts to login with the above email address and this user does not exist inside of your LDAP directory, then standard Eloquent authentication will be performed instead.

This feature is ideal for environments where:

  • LDAP server connectivity may be intermittent
  • Or; You have regular users registering normally in your application

If you would like your LDAP users to be able to sign in to your application when LDAP connectivity fails or is not present, you must enable the sync passwords option, so your LDAP users can sign in using their last successfully used password.

If an LDAP users password has not been synchronized, they will not be able to sign in.

Eloquent Model Binding

If you are using database synchronization, model binding allows you to access the currently authenticated user's LdapRecord model from their Eloquent model. This grants you access to their LDAP data whenever you need it.

To begin, insert the LdapRecord\Laravel\Auth\HasLdapUser trait onto your User model:

// app/Models/User.php

// ...
use LdapRecord\Laravel\Auth\HasLdapUser;
use LdapRecord\Laravel\Auth\LdapAuthenticatable;
use LdapRecord\Laravel\Auth\AuthenticatesWithLdap;

class User extends Authenticatable implements LdapAuthenticatable
{
    //...

    use HasLdapUser;

    // ...
}

Now, after an LDAP user logs into your application, their LdapRecord model will be available on their Eloquent model via the ldap property:

If their LDAP model cannot be located, the returned value will be null.

// Instance of App\User
$user = Auth::user();

// Instance of App\Ldap\User
$user->ldap;

// Get LDAP user attributes
echo $user->ldap->getFirstAttribute('cn');

// Get LDAP user relationships:
$groups = $user->ldap->groups()->get();

This property uses deferred loading -- which means that the users LDAP model only gets requested from your server when you attempt to access it. This prevents loading the model unnecessarily when it is not needed in your application.

Displaying LDAP Error Messages

When using Laravel Jetstream, LDAP error messages will now be displayed automatically to users. You do not need to configure or include the ListensForLdapBindFailure trait as you would using Laravel UI on the LoginController.

Altering the Response

Since this functionality is now automatically registered, if you would like to modify how an error is handled, call the setErrorHandler method on the BindFailureListener class inside of your AuthServiceProvider.php file:

// app/Providers/AuthServiceProvider.php

// ...
use LdapRecord\Laravel\Auth\BindFailureListener;

class AuthServiceProvider extends ServiceProvider
{
    // ...

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

        BindFailureListener::setErrorHandler(function ($message, $code = null) {
            if ($code == '773') {
                // The users password has expired. Redirect them.
                abort(redirect('/password-reset'));
            }
        });
    }
}

Refer to the Password Policy Errors documentation to see what each code means.

Changing the Error Messages

If you need to modify the translations of these error messages, create a new translation file named errors.php in your resources directory at the following path:

The vendor directory (and each sub-directory) will have to be created manually.

resources
lang
vendor
ldap
en
errors.php

Then, paste in the following translations in the file and modify where necessary:

<?php

return [
    'user_not_found' => 'User not found.',
    'user_not_permitted_at_this_time' => 'Not permitted to logon at this time.',
    'user_not_permitted_to_login' => 'Not permitted to logon at this workstation.',
    'password_expired' => 'Your password has expired.',
    'account_disabled' => 'Your account is disabled.',
    'account_expired' => 'Your account has expired.',
    'user_must_reset_password' => 'You must reset your password before logging in.',
    'user_account_locked' => 'Your account is locked.',
];
← Previous Topic

Setup

Next Topic →

Laravel UI