Ldaprecord with filament

Has anyone implemented ldaprecord with filament? I have created a page, I have overridden the default login to use username instead of email, i had implemented ldap record in the app before i installed filament and it was working fine, so how can i tell filament to use ldap, i have tried to implement LdapAuthenticatable on the custom Login but it still says credentials dont match our records
11 Replies
Dennis Koch
Dennis Koch14mo ago
Not sure how your LDAP implementation works. You can change the auth guard which Filament uses
kenromdavids
kenromdavids14mo ago
user model 'class User extends Authenticatable implements LdapAuthenticatable' in auth.php i changed the guard to 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'ldap', ], ], under providers 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], 'ldap' => [ 'driver' => 'ldap', 'model' => LdapRecord\Models\ActiveDirectory\User::class, 'rules' => [], 'database' => [ 'model' => App\Models\User::class, 'sync_passwords' => true, 'sync_attributes' => [ 'name' => 'cn', 'email' => 'mail', 'username' => 'samaccountname', ], 'sync_existing' => [ 'email' => 'mail', ], 'password_column' => 'password', ], ], // 'users' => [ // 'driver' => 'database', // 'table' => 'users', // ], ], Basically i use username and the guard is ldap
aliaxon
aliaxon11mo ago
I'm also looking for the same possibility: https://ldaprecord.com/ this has an extensive documentation for Jetstream, Breeze. It would be great if some documentation is extended from filament core team
LdapRecord
LdapRecord is a framework that helps you quickly integrate LDAP into your PHP applications.
pyr0t0n
pyr0t0n11mo ago
Did one of you get this to work?
kenromdavids
kenromdavids11mo ago
I got this working
//I created a Login.php in App\Filament\Auth
<?php
namespace App\Filament\Auth;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Filament\Facades\Filament;
use Filament\Forms\Form;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Component;
use Filament\Http\Responses\Auth\Contracts\LoginResponse;
use Filament\Notifications\Notification;
use Filament\Pages\Auth\Login as BaseAuth;
use Illuminate\Validation\ValidationException;

class Login extends BaseAuth
{
/*content in the next message*/
}
//I created a Login.php in App\Filament\Auth
<?php
namespace App\Filament\Auth;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Filament\Facades\Filament;
use Filament\Forms\Form;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Component;
use Filament\Http\Responses\Auth\Contracts\LoginResponse;
use Filament\Notifications\Notification;
use Filament\Pages\Auth\Login as BaseAuth;
use Illuminate\Validation\ValidationException;

class Login extends BaseAuth
{
/*content in the next message*/
}
// this is the content
public function form(Form $form): Form
{
return $form
->schema([
//$this->getEmailFormComponent(), /*i commented out email since i wanted to use username*/
$this->getUsernameFormComponent(),
$this->getPasswordFormComponent(),
$this->getRememberFormComponent(),
])
->statePath('data');
}

protected function getUsernameFormComponent(): Component
{
return TextInput::make('username')
->label('username')
->required()
->autocomplete()
->autofocus();
}
protected function getCredentialsFromFormData(array $data): array
{
return [
'username' => $data['username'],
'password' => $data['password'],
];
}
public function authenticate(): ?LoginResponse
{
try {
$this->rateLimit(5);
} catch (TooManyRequestsException $exception) {
$this->addError('username', __('filament::login.messages.throttled', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]));

return null;
}

$data = $this->form->getState();

if (! Filament::auth()->attempt([
'samaccountname' => $data['username'], /*changed to samaccountname */
'password' => $data['password'],
], $data['remember'])) {
$this->addError('username', __('filament::login.messages.failed'));

return null;
}

return app(LoginResponse::class);
}
// this is the content
public function form(Form $form): Form
{
return $form
->schema([
//$this->getEmailFormComponent(), /*i commented out email since i wanted to use username*/
$this->getUsernameFormComponent(),
$this->getPasswordFormComponent(),
$this->getRememberFormComponent(),
])
->statePath('data');
}

protected function getUsernameFormComponent(): Component
{
return TextInput::make('username')
->label('username')
->required()
->autocomplete()
->autofocus();
}
protected function getCredentialsFromFormData(array $data): array
{
return [
'username' => $data['username'],
'password' => $data['password'],
];
}
public function authenticate(): ?LoginResponse
{
try {
$this->rateLimit(5);
} catch (TooManyRequestsException $exception) {
$this->addError('username', __('filament::login.messages.throttled', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]));

return null;
}

$data = $this->form->getState();

if (! Filament::auth()->attempt([
'samaccountname' => $data['username'], /*changed to samaccountname */
'password' => $data['password'],
], $data['remember'])) {
$this->addError('username', __('filament::login.messages.failed'));

return null;
}

return app(LoginResponse::class);
}
toeknee
toeknee11mo ago
Legend! Needed this tomorrow for a project
kenromdavids
kenromdavids11mo ago
I then added Login class in AdminPanelProvider.php
use App\Filament\Auth\Login;
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login(Login::class) //only added this
->colors([
'primary' => Color::Amber,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
use App\Filament\Auth\Login;
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login(Login::class) //only added this
->colors([
'primary' => Color::Amber,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
toeknee
toeknee11mo ago
How are you syncing the user to Filament from LDAP?
kenromdavids
kenromdavids11mo ago
in my auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'ldap',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'ldap' => [
'driver' => 'ldap',
'model' => LdapRecord\Models\ActiveDirectory\User::class,
'rules' => [],
'database' => [
'model' => App\Models\User::class,
'sync_passwords' => true,
'sync_attributes' => [
'name' => 'cn',
'email' => 'mail',
'username' => 'samaccountname',
],

'sync_existing' => [
'email' => 'mail',

],
'password_column' => 'password',
],
],

],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'ldap',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'ldap' => [
'driver' => 'ldap',
'model' => LdapRecord\Models\ActiveDirectory\User::class,
'rules' => [],
'database' => [
'model' => App\Models\User::class,
'sync_passwords' => true,
'sync_attributes' => [
'name' => 'cn',
'email' => 'mail',
'username' => 'samaccountname',
],

'sync_existing' => [
'email' => 'mail',

],
'password_column' => 'password',
],
],

],
I edited FortifyServiceProvider.php in providers like this since am using jetstream and fortify
public function boot(): void
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
/*added this*/
Fortify::authenticateUsing(function ($request) {
$validated = Auth::validate([
'samaccountname' => $request->username,
'password' => $request->password
]);

return $validated ? Auth::getLastAttempted() : null;
});
/*ends here*/
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());

return Limit::perMinute(5)->by($throttleKey);
});

RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
public function boot(): void
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
/*added this*/
Fortify::authenticateUsing(function ($request) {
$validated = Auth::validate([
'samaccountname' => $request->username,
'password' => $request->password
]);

return $validated ? Auth::getLastAttempted() : null;
});
/*ends here*/
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());

return Limit::perMinute(5)->by($throttleKey);
});

RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
the User.php model is like this
class User extends Authenticatable implements LdapAuthenticatable
{
use HasApiTokens;
use HasFactory;
use HasProfilePhoto;
use Notifiable;
use TwoFactorAuthenticatable;
use AuthenticatesWithLdap;

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'username',
'email',
'password',
];
/*more code left out because of less characters
}
class User extends Authenticatable implements LdapAuthenticatable
{
use HasApiTokens;
use HasFactory;
use HasProfilePhoto;
use Notifiable;
use TwoFactorAuthenticatable;
use AuthenticatesWithLdap;

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'username',
'email',
'password',
];
/*more code left out because of less characters
}
Jonathan Tyler
Jonathan Tyler7mo ago
I'm using email instead of username as I wanted both local and LDAP auth. I got this working similar to above, just replacing the getCredentialsFromFormData function, however I tried to do with fallback (as is done with Fortify, etc) but it's not working as expected. Local auth just doesn't work. Any thoughts as to why fallback doesn't work here?
protected function getCredentialsFromFormData(array $data): array
{
return [
'mail' => $data['email'],
'password' => $data['password'],
'fallback' => [
'email' => $data['email'],
'password' => $data['password'],
],
];
}
protected function getCredentialsFromFormData(array $data): array
{
return [
'mail' => $data['email'],
'password' => $data['password'],
'fallback' => [
'email' => $data['email'],
'password' => $data['password'],
],
];
}
Hmm, never mind... it appears my migrations weren't updating users table correctly. After dropping and re-creating I can now login with username or email (ldap or local). I've modified my custom Login class (similar to @kenro's setup), see next reply. Specifically Filament::auth()->attempt() is using the getCredentialsFromFormData, with logic to determine if username or email, and I added the 'tabindex' on the username field as it was skipping the password field when tabbing over. Many thanks to @kenro for your post putting me on the right direction!!
namespace App\Filament\Auth;

use Filament\Pages\Auth\Login as BaseAuth;
#...

class Login extends BaseAuth
{
public function form(Form $form): Form
{
#...
}

protected function getUsernameFormComponent(): Component
{
return TextInput::make('username')
->label('Username or Email')
->required()
->autocomplete()
->autofocus()
->extraInputAttributes(['tabindex' => 1]);
}

protected function getCredentialsFromFormData(array $data): array
{
$ldapKey = "samaccountname";
$localKey = "username";

if (filter_var($data['username'], FILTER_VALIDATE_EMAIL)) {
$ldapKey = "mail";
$localKey = "email";
}

return [
$ldapKey => $data['username'],
'password' => $data['password'],
'fallback' => [
$localKey => $data['username'],
'password' => $data['password'],
],
];
}

public function authenticate(): ?LoginResponse
{
#...

if (!Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'])) {
$this->addError('username', __('filament::login.messages.failed'));

return null;
}

return app(LoginResponse::class);
}
}
namespace App\Filament\Auth;

use Filament\Pages\Auth\Login as BaseAuth;
#...

class Login extends BaseAuth
{
public function form(Form $form): Form
{
#...
}

protected function getUsernameFormComponent(): Component
{
return TextInput::make('username')
->label('Username or Email')
->required()
->autocomplete()
->autofocus()
->extraInputAttributes(['tabindex' => 1]);
}

protected function getCredentialsFromFormData(array $data): array
{
$ldapKey = "samaccountname";
$localKey = "username";

if (filter_var($data['username'], FILTER_VALIDATE_EMAIL)) {
$ldapKey = "mail";
$localKey = "email";
}

return [
$ldapKey => $data['username'],
'password' => $data['password'],
'fallback' => [
$localKey => $data['username'],
'password' => $data['password'],
],
];
}

public function authenticate(): ?LoginResponse
{
#...

if (!Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'])) {
$this->addError('username', __('filament::login.messages.failed'));

return null;
}

return app(LoginResponse::class);
}
}
Positiverain
Positiverain7mo ago
in providers -> ldap -> model, don't forget to use
'providers' => [
'ldap' => [
'model' => LdapRecord\Models\OpenLDAP\User::class,
.......
'providers' => [
'ldap' => [
'model' => LdapRecord\Models\OpenLDAP\User::class,
.......
when using Online LDAP test server or any other OpenLDAP. source: https://www.youtube.com/watch?v=5lRBGdLrxj0
Abdelhalim Saïdi
YouTube
Step 2 : Database auth - install and config - Ldaprecord v2 - larav...
Now we should create and connect our database than test it and we will leave the chapter of Laravel breeze at the last video