Laravel Breeze
Introduction
Important: Before getting started, please complete the below guides:
Laravel Breeze provides basic authentication scaffolding out-of-the-box.
This guide will show you how to integrate LdapRecord-Laravel using this scaffolding.
Debugging
Inside of 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
Login Request
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 the authenticate
method in the LoginRequest
, and
pass in an array with the mail
key (which is the attribute we are
wanting to retrieve our LDAP users by) and the users password
:
// app/Http/Requests/Auth/LoginRequest.php
/**
* Attempt to authenticate the request's credentials.
*
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate()
{
$this->ensureIsNotRateLimited();
$credentials = [
'mail' => $this->email,
'password' => $this->password,
];
if (! Auth::attempt($credentials, $this->filled('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
That's it! You are now ready to authenticate LDAP users into your application.
Using Usernames
To authenticate your users by their username we must adjust some scaffolded code generated by Laravel Breeze.
In the following example, we will authenticate users by their sAMAccountName
.
Sync Attributes
We will first need to adjust our sync_attributes
array, located inside of our config/auth.php
file.
The only thing we need to change is the email
key:
From:
// config/auth.php
'sync_attributes' => [
// ...
'email' => 'mail',
],
To:
// config/auth.php
'sync_attributes' => [
// ...
'username' => 'samaccountname',
],
User Migration
Now that we have adjusted our synchronized attributes, we need to adjust the users
database table migration.
Similarly as above, we only need to change the email
column to username
:
From:
Schema::create('users', function (Blueprint $table) {
// ...
$table->string('email')->unique();
// ...
});
To:
Schema::create('users', function (Blueprint $table) {
// ...
$table->string('username')->unique();
// ...
});
Login Form
We're almost there. We will now need to update the input HTML field inside of the scaffolded login.blade.php
view:
From:
<!-- resources/views/auth/login.blade.php -->
<!-- Email Address -->
<div>
<x-label for="email" :value="__('Email')" />
<x-input
id="email"
class="block w-full mt-1"
type="email"
name="email"
:value="old('email')"
required
autofocus
/>
</div>
To:
<!-- resources/views/auth/login.blade.php -->
<!-- Username -->
<div>
<x-label for="username" :value="__('Username')" />
<x-input
id="username"
class="block w-full mt-1"
type="text"
name="username"
:value="old('username')"
required
autofocus
/>
</div>
Login Request
This last step requires adjusting the rules()
and authenticate()
methods inside of the scaffolded LoginRequest.php
class:
From:
// app/Http/Requests/Auth/LoginRequest.php
public function rules()
{
return [
'email' => 'required|string|email',
'password' => 'required|string',
];
}
public function authenticate()
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($request->only('email', 'password'), $this->filled('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
To:
// app/Http/Requests/Auth/LoginRequest.php
public function rules()
{
return [
'username' => 'required|string',
'password' => 'required|string',
];
}
public function authenticate()
{
$this->ensureIsNotRateLimited();
$credentials = [
'samaccountname' => $this->username,
'password' => $this->password,
];
if (! Auth::attempt($credentials, $this->filled('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
You are now ready to login LDAP users by their username!
Fallback Authentication
Database fallback allows the authentication of local database users if:
- LDAP connectivity is not present.
- Or; An LDAP user cannot be found.
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 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.
To enable this feature, you must define a fallback
array inside of the $credentials
you pass to the Auth::attempt()
method inside of your LoginRequest
:
// app/Http/Requests/Auth/LoginRequest.php
public function authenticate()
{
$this->ensureIsNotRateLimited();
$credentials = [
'mail' => $this->email,
'password' => $this->password,
'fallback' => [
'email' => $this->email,
'password' => $this->password,
],
];
if (! Auth::attempt($credentials, $this->filled('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
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 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
eloquent 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 HasFactory, Notifiable, AuthenticatesWithLdap, 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 will be
null
.
// Instance of App\Models\User:
$user = Auth::user();
// Instance of LdapRecord\Models\Model:
$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 a user fails LDAP authentication due to their password / account expiring, account lockout, or their password requiring to be changed, specific error codes will be sent back from your server. LdapRecord can interpret these for you and display helpful error messages to users upon failing authentication.
To enable this feature, you will have to:
- Navigate to the scaffolded
AuthenticatedSessionController.php
- Insert the
ListensForLdapBindFailure
trait - Call the
listenForLdapBindFailure()
method in the constructor:
// app/Http/Controllers/Auth/AuthenticatedSessionController.php
use LdapRecord\Laravel\Auth\ListensForLdapBindFailure;
class AuthenticatedSessionController extends Controller
{
use ListensForLdapBindFailure;
public function __construct()
{
$this->listenForLdapBindFailure();
}
// ...
}
Changing The Input Field
By default, LdapRecord-Laravel will attach the LDAP error to the email
input field.
If you're using a different input field, you can customize it by adding a username
property to the AuthenticatedSessionController
:
use LdapRecord\Laravel\Auth\ListensForLdapBindFailure;
class AuthenticatedSessionController extends Controller
{
use ListensForLdapBindFailure;
protected $username = 'username';
public function __construct()
{
$this->listenForLdapBindFailure();
}
// ...
}
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.
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.',
];
Altering the Response
By default, when an LDAP bind failure occurs, a ValidationException
will be thrown which will
redirect users to your login page and display the error. If you would like to modify this
behaviour, you will need to override the method handleLdapBindError
.
This method will include the error $message
as the first parameter and the error $code
as the second.
This is useful for checking for specific Active Directory response codes and returning a response:
use Illuminate\Validation\ValidationException;
use LdapRecord\Laravel\Auth\ListensForLdapBindFailure;
class AuthenticatedSessionController extends Controller
{
use ListensForLdapBindFailure;
protected function handleLdapBindError($message, $code = null)
{
if ($code == '773') {
// The users password has expired. Redirect them.
abort(redirect('/password-reset'));
}
throw ValidationException::withMessages([
'email' => "Whoops! LDAP server cannot be reached.",
]);
}
// ...
}
Refer to the Password Policy Errors documentation to see what each code means.