Customizing Filament’s Login to use LDAP
This is a quick rough draft post for how to override the default Filament login and use your own to login with LDAP.
Tested With:
- Laravel 9
- Filament 2
- ldaprecord-laravel 2.6
Notes:
I’m using Filament 2 but it should work the same in 3. But check the code and refer to the docs if you run into trouble. There’s not a lot of super custom code here so this should work with updated versions of all the packages used.
I’m using ldaprecord-laravel package 2.x in this example as well. In my use case I only needed to authenticate against our LDAP login. Using ldaprecord may be overkill in my use case but I expected there to be more requirements at some point. If you need to sync anything from the LDAP user model ldaprecord will let you do that.
Could we do this with just some event listener or other way instead of overriding the Filament Login.php? That would be better so we don’t have to keep our version of Login.php current with any changes to the standard one provided by Filament. I never got a chance to look into this but it seems like there would be a way. Feel free to shout in the comments if you know. I’m using the directorytree/ldaprecord-laravel package in this code, so you’ll need to:
1composer require directorytree/ldaprecord-laravel
Make sure to go star the repo. Steve is a really sharp developer and all around nice guy.
Follow the instructions to setup your LDAP configuration in Laravel. It’s pretty standard Laravel stuff here. Make sure to configure your ldap connection per the instructions for this package. We’ll be using these variables to connect and verify that the user provided valid credentials.
Then you just need to override the default login by creating our own here:
1app/Filament/Pages/Auth/Login.php
Below is what this code looks like. It’s a combination of the default login from Filament with the ldap stuff doing the job of validating the creds, then we just log the user in the standard Laravel way.
Below is the code, commented for clarity.
That should do it. Let me know if I missed anything or if you have suggestions for improvement.
1<?php 2namespace App\Filament\Pages\Auth; 3 4use App\Models\User; 5use Exception; 6use Filament\Forms\Components\Checkbox; 7use Filament\Forms\Components\TextInput; 8use Filament\Forms\Components\ViewField; 9use Filament\Forms\Contracts\HasForms; 10use Filament\Http\Livewire\Auth\Login as BasePage; 11use Filament\Http\Responses\Auth\Contracts\LoginResponse; 12use Illuminate\Support\Facades\Auth; 13use Illuminate\Support\Facades\Hash; 14use Illuminate\Support\Str; 15use LdapRecord\Connection; 16use LdapRecord\Container; 17 18class Login extends BasePage implements HasForms { 19 20 public $username = ''; 21 public $password = ''; 22 public $remember = false; 23 24 public function authenticate() : ?LoginResponse { 25 // Implement rate limiting to protect against brute force attacks 26 try { 27 $this->rateLimit(5); 28 } catch (TooManyRequestsException $exception) { 29 $this->addError('username', __('filament::login.messages.throttled', [ 30 'seconds' => $exception->secondsUntilAvailable, 31 'minutes' => ceil($exception->secondsUntilAvailable / 60), 32 ])); 33 return null; 34 } 35 36 $data = $this->form->getState(); 37 38 // Prefix for LDAP user names (corp\\username) 39 $ldap_user = 'corp' . '\\' . $data['username']; 40 $ldap_pass = $data['password']; 41 42 // Use environment variables for LDAP configuration 43 $connection = new Connection([ 44 'hosts' => [config('LDAP_HOST', 'your_default_host')], 45 'port' => config('LDAP_PORT', 389), 46 'base_dn' => config('LDAP_BASE_DN', 'DC=corp,DC=your_domain,DC=com'), 47 'username' => $ldap_user, 48 'password' => $ldap_pass, 49 ]); 50 51 Container::addConnection($connection); 52 53 // Attempt LDAP authentication 54 try { 55 $connection->auth()->attempt($ldap_user, $ldap_pass); 56 } catch (Exception $e) { 57 // Provide a more specific error message for authentication failure 58 $this->addError('username', __('Authentication failed, please try again.')); 59 $this->addError('password', __('Authentication failed, please try again.')); 60 return null; 61 } 62 63 if (get_class($connection) === Connection::class) { 64 $query = $connection->query(); 65 // Retrieve LDAP record, add any additional logic you want to check for a valid/active user here 66 $ldap_record = $query->where('your_ldap_account_field_name', '=', $data['username'])->first(); 67 68 69 // Create or update the local user 70 $user = User::updateOrCreate([ 71 'email' => $ldap_record['your_ldap_email_field_name'][0], 72 ], [ 73 'name' => $ldap_record['cn'][0], 74 'password' => Hash::make(Str::random(10)), // Hash a random string for password 75 ]); 76 77 Auth::login($user, $this->remember); 78 79 // Additional steps for user model or login process can be added here 80 81 // Return the expected LoginResponse class 82 return app(LoginResponse::class); 83 } 84 85 return null; 86 } 87 88 protected function getFormSchema() : array { 89 return [ 90 ViewField::make('login-notice')->view('filament.pages.auth.login.header'), 91 TextInput::make('username') 92 ->label(__('Username')) 93 ->required() 94 ->autocomplete(), 95 TextInput::make('password') 96 ->label(__('filament::login.fields.password.label')) 97 ->password() 98 ->required(), 99 Checkbox::make('remember')100 ->label(__('filament::login.fields.remember.label')),101 ];102 }103 104}
In the above code you could implement the user sync from ldap to your local database. That was not a requirement for my use case. But it’s documented in the ldaprecord package if you need that
No comments yet…