Laravel Jetstream
Introduction
Important: Before getting started, please complete the below guides:
Laravel Jetstream provides robust authentication scaffolding out-of-the-box. It utilizes Laravel Fortify for authentication under the hood.
We will customize various aspects of Jetsream and Fortify to allow LDAP users to sign in to the application.
Debugging
inside your config/ldap.php
file, ensure you have logging
enabled during the setup of authentication.
Doing this will help you immensely in debugging connectivity and authentication issues.
If you encounter issues along the way, be sure to open your storage/logs
directory after you
attempt signing in to your application and see what issues may be occurring.
In addition, you may also run the below artisan command to test connectivity to your LDAP server:
php artisan ldap:test
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 AppServiceProvider.php
file:
// app/Providers/AppServiceProvider.php
// ...
use Laravel\Fortify\Fortify;
use Illuminate\Support\Facades\Auth;
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot(): void
{
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 LDAP 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. Thepassword
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 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 set up 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 AppServiceProvider.php
file
and set up our authentication callback using the Fortify::authenticateUsing()
method:
// app/Providers/AppServiceProvider.php
// ...
use Laravel\Fortify\Fortify;
use Illuminate\Support\Facades\Auth;
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot(): void
{
Fortify::authenticateUsing(function ($request) {
$validated = Auth::validate([
'samaccountname' => $request->username,
'password' => $request->password
]);
return $validated ? Auth::getLastAttempted() : null;
});
}
}
Username Configuration
inside 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 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
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' => [
// ...
'users' => [
// ...
'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:
<!-- Before: -->
<div>
<x-jet-label value="Email" />
<x-jet-input
class="block w-full mt-1"
type="email"
name="email"
:value="old('email')"
required
autofocus
/>
</div>
<!-- After: -->
<div>
<x-jet-label value="Username" />
<x-jet-input
class="block w-full mt-1"
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 the credentials
you insert into the Auth::validate()
method in your Fortify::authenticateUsing()
callback:
// app/Providers/AppServiceProvider.php
use Laravel\Fortify\Fortify;
use Illuminate\Support\Facades\Auth;
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot(): void
{
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 | password | guid | domain | |
---|---|---|---|---|---|
1 | Steve Bauman | sbauman@outlook.com | ... | null |
null |
If a user attempts to log in with the above email address and this user does not exist inside 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
Model binding allows you to access the currently authenticated user's LdapRecord model from their Eloquent model. This grants you access to their LDAP model 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\Models\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 your AppServiceProvider.php
file:
// app/Providers/AppServiceProvider.php
// ...
use LdapRecord\Laravel\Auth\BindFailureListener;
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot(): void
{
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 subdirectory) will have to be created manually.
Laravel >= 9
lang/
└── vendor/
└── ldap/
└── en/
└── errors.php
Laravel <= 8
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.',
];
Teams
Default Team Assignment
Since LDAP users are not registered through Jetstream's interface and are instead created
through an import or successful authentication, you will have to assign their default
team by utilizing LdapRecord's Imported
event, which is fired directly after a
new user has been imported or created inside your application's database.
Create the event listener executing the below command:
php artisan make:listener AssignTeam --event="LdapRecord\Laravel\Events\Import\Imported"
inside the event listener, attach the users team as you would during a normal users registration through the registration interface:
namespace App\Listeners;
use App\Models\Team;
use LdapRecord\Laravel\Events\Import\Imported;
class AssignTeam
{
/**
* Handle the event.
*
* @param Imported $event
*
* @return void
*/
public function handle(Imported $event)
{
$user = $event->eloquent;
$user->ownedTeams()->save(Team::forceCreate([
'user_id' => $user->id,
'name' => explode(' ', $user->name, 2)[0]."'s Team",
'personal_team' => true,
]));
}
}
Finally, register the event inside your EventServiceProvider
:
// app/Providers/EventServiceProvider.php
use App\Listeners\AssignTeam;
use LdapRecord\Laravel\Events\Import\Imported;
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
Imported::class => [
AssignTeam::class,
],
];