├── .github └── issue_template.md ├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── .travis.yml ├── composer.json ├── docs ├── .nojekyll ├── _sidebar.md ├── auth.md ├── auth │ ├── events.md │ ├── importing.md │ ├── installation.md │ ├── introduction.md │ ├── middleware.md │ ├── model-binding.md │ ├── setup.md │ └── testing.md ├── index.html ├── installation.md ├── readme.md ├── setup.md ├── tutorials │ ├── basics.md │ └── users.md ├── upgrading.md └── usage.md ├── license.md ├── phpunit.xml ├── readme.md ├── src ├── AdldapAuthServiceProvider.php ├── AdldapServiceProvider.php ├── Auth │ ├── DatabaseUserProvider.php │ ├── ForwardsCalls.php │ ├── NoDatabaseUserProvider.php │ └── UserProvider.php ├── Commands │ ├── Console │ │ └── Import.php │ ├── Import.php │ ├── SyncPassword.php │ └── UserImportScope.php ├── Config │ ├── auth.php │ └── config.php ├── Events │ ├── Authenticated.php │ ├── AuthenticatedModelTrashed.php │ ├── AuthenticatedWithCredentials.php │ ├── AuthenticatedWithWindows.php │ ├── Authenticating.php │ ├── AuthenticationFailed.php │ ├── AuthenticationRejected.php │ ├── AuthenticationSuccessful.php │ ├── DiscoveredWithCredentials.php │ ├── Imported.php │ ├── Importing.php │ ├── Synchronized.php │ └── Synchronizing.php ├── Facades │ ├── Adldap.php │ └── Resolver.php ├── Listeners │ ├── BindsLdapUserModel.php │ ├── LogAuthenticated.php │ ├── LogAuthentication.php │ ├── LogAuthenticationFailure.php │ ├── LogAuthenticationRejection.php │ ├── LogAuthenticationSuccess.php │ ├── LogDiscovery.php │ ├── LogImport.php │ ├── LogSynchronized.php │ ├── LogSynchronizing.php │ ├── LogTrashedModel.php │ └── LogWindowsAuth.php ├── Middleware │ └── WindowsAuthenticate.php ├── Resolvers │ ├── ResolverInterface.php │ └── UserResolver.php ├── Scopes │ ├── MemberOfScope.php │ ├── ScopeInterface.php │ ├── UidScope.php │ └── UpnScope.php ├── Traits │ ├── HasLdapUser.php │ └── ValidatesUsers.php └── Validation │ ├── Rules │ ├── DenyTrashed.php │ ├── OnlyImported.php │ └── Rule.php │ └── Validator.php └── tests ├── Console └── ImportTest.php ├── DatabaseImporterTest.php ├── DatabaseProviderTest.php ├── DatabaseTestCase.php ├── EloquentAuthenticateTest.php ├── Handlers └── LdapAttributeHandler.php ├── HasLdapUserTest.php ├── Listeners ├── LogAuthenticatedTest.php ├── LogAuthenticationFailureTest.php ├── LogAuthenticationRejectionTest.php ├── LogAuthenticationSuccessTest.php ├── LogAuthenticationTest.php ├── LogDiscoveryTest.php ├── LogImportTest.php ├── LogSynchronizedTest.php ├── LogSynchronizingTest.php ├── LogTrashedModelTest.php └── LogWindowsAuthTest.php ├── Models └── TestUser.php ├── NoDatabaseProviderTest.php ├── NoDatabaseTestCase.php ├── Scopes └── JohnDoeScope.php ├── TestCase.php ├── UserResolverTest.php └── WindowsAuthenticateTest.php /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | - Laravel Version: #.# 2 | - Adldap2-Laravel Version: #.# 3 | - PHP Version: #.# 4 | - LDAP Type: 5 | 6 | 7 | 8 | ### Description: 9 | 10 | 11 | 12 | ### Steps To Reproduce: 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - tests/* 4 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | 8 | before_script: 9 | - travis_retry composer self-update 10 | - travis_retry composer install --prefer-source --no-interaction 11 | 12 | script: ./vendor/bin/phpunit 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adldap2/adldap2-laravel", 3 | "description": "LDAP Authentication & Management for Laravel.", 4 | "keywords": [ 5 | "adldap", 6 | "adldap2", 7 | "ldap", 8 | "laravel" 9 | ], 10 | "license": "MIT", 11 | "type": "project", 12 | "require": { 13 | "php": ">=7.1", 14 | "adldap2/adldap2": "^10.1", 15 | "illuminate/support": "~5.5|~6.0|~7.0|~8.0|~9.0|^10.0" 16 | }, 17 | "require-dev": { 18 | "mockery/mockery": "~1.0", 19 | "phpunit/phpunit": "~7.0|~8.0|^9.5.10", 20 | "orchestra/testbench": "~3.7|~4.0|^8.0" 21 | }, 22 | "archive": { 23 | "exclude": [ 24 | "/tests" 25 | ] 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Adldap\\Laravel\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Adldap\\Laravel\\Tests\\": "tests/" 35 | } 36 | }, 37 | "extra": { 38 | "laravel": { 39 | "providers": [ 40 | "Adldap\\Laravel\\AdldapServiceProvider", 41 | "Adldap\\Laravel\\AdldapAuthServiceProvider" 42 | ], 43 | "aliases": { 44 | "Adldap": "Adldap\\Laravel\\Facades\\Adldap" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adldap2/Adldap2-Laravel/8637098805cff9fcbb32d9eeef6aba0e939a8fcb/docs/.nojekyll -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * Getting Started 2 | 3 | * [Introduction & Quick Start](/) 4 | * [Installation](installation.md) 5 | * [Setup](setup.md) 6 | * [Usage](usage.md) 7 | * [Upgrade Guide](upgrading.md) 8 | 9 | * Authentication Driver 10 | 11 | * [Introduction & Quick Start](auth/introduction.md) 12 | * [Installation](auth/installation.md) 13 | * [Setup](auth/setup.md) 14 | * [Middleware (Single Sign On)](auth/middleware.md) 15 | * [Events](auth/events.md) 16 | * [Model Binding](auth/model-binding.md) 17 | * [Importing](auth/importing.md) 18 | * [Testing](auth/testing.md) 19 | -------------------------------------------------------------------------------- /docs/auth.md: -------------------------------------------------------------------------------- 1 | # Auth Driver 2 | 3 | #### Developing Locally without an LDAP connection 4 | 5 | You can continue to develop and login to your application without a 6 | connection to your LDAP server in the following scenario: 7 | 8 | * You have `auto_connect` set to `false` in your `ldap.php` configuration 9 | > This is necessary so we don't automatically try and bind to your LDAP server when your application boots. 10 | 11 | * You have `login_fallback` set to `true` in your `ldap_auth.php` configuration 12 | > This is necessary so we fallback to the standard `eloquent` auth driver. 13 | 14 | * You have `password_sync` set to `true` in your `ldap_auth.php` configuration 15 | > This is necessary so we can login to the account with the last password that was used when an LDAP connection was present. 16 | 17 | * You have logged into the synchronized LDAP account previously 18 | > This is necessary so the account actually exists in your local app's database. 19 | 20 | If you have this configuration, you will have no issues developing an 21 | application without a persistent connection to your LDAP server. -------------------------------------------------------------------------------- /docs/auth/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | Adldap2-Laravel raises a variety of events throughout authentication attempts. 4 | 5 | You may attach listeners to these events in your `EventServiceProvider`: 6 | 7 | ```php 8 | /** 9 | * The event listener mappings for the application. 10 | * 11 | * @var array 12 | */ 13 | protected $listen = [ 14 | 15 | 'Adldap\Laravel\Events\Authenticating' => [ 16 | 'App\Listeners\LogAuthenticating', 17 | ], 18 | 19 | 'Adldap\Laravel\Events\Authenticated' => [ 20 | 'App\Listeners\LogLdapAuthSuccessful', 21 | ], 22 | 23 | 'Adldap\Laravel\Events\AuthenticationSuccessful' => [ 24 | 'App\Listeners\LogAuthSuccessful' 25 | ], 26 | 27 | 'Adldap\Laravel\Events\AuthenticationFailed' => [ 28 | 'App\Listeners\LogAuthFailure', 29 | ], 30 | 31 | 'Adldap\Laravel\Events\AuthenticationRejected' => [ 32 | 'App\Listeners\LogAuthRejected', 33 | ], 34 | 35 | 'Adldap\Laravel\Events\AuthenticatedModelTrashed' => [ 36 | 'App\Listeners\LogUserModelIsTrashed', 37 | ], 38 | 39 | 'Adldap\Laravel\Events\AuthenticatedWithCredentials' => [ 40 | 'App\Listeners\LogAuthWithCredentials', 41 | ], 42 | 43 | 'Adldap\Laravel\Events\AuthenticatedWithWindows' => [ 44 | 'App\Listeners\LogSSOAuth', 45 | ], 46 | 47 | 'Adldap\Laravel\Events\DiscoveredWithCredentials' => [ 48 | 'App\Listeners\LogAuthUserLocated', 49 | ], 50 | 51 | 'Adldap\Laravel\Events\Importing' => [ 52 | 'App\Listeners\LogImportingUser', 53 | ], 54 | 55 | 'Adldap\Laravel\Events\Synchronized' => [ 56 | 'App\Listeners\LogSynchronizedUser', 57 | ], 58 | 59 | 'Adldap\Laravel\Events\Synchronizing' => [ 60 | 'App\Listeners\LogSynchronizingUser', 61 | ], 62 | 63 | ]; 64 | ``` 65 | 66 | > **Note:** For some real examples, you can browse the listeners located 67 | > in: `vendor/adldap2/adldap2-laravel/src/Listeners` and see their usage. -------------------------------------------------------------------------------- /docs/auth/importing.md: -------------------------------------------------------------------------------- 1 | # Importing 2 | 3 | Adldap2-Laravel comes with a command that allows you to import users from your LDAP server automatically. 4 | 5 | > **Note**: Make sure you're able to connect to your LDAP server and have configured 6 | > the `ldap` auth driver correctly before running the command. 7 | 8 | ## Running the Command 9 | 10 | To import all users from your LDAP connection simply run `php artisan adldap:import`. 11 | 12 | > **Note**: The import command will utilize all scopes and sync all attributes you 13 | > have configured in your `config/ldap_auth.php` configuration file. 14 | 15 | Example: 16 | 17 | ```bash 18 | php artisan adldap:import 19 | 20 | Found 2 user(s). 21 | ``` 22 | 23 | You will then be asked: 24 | 25 | ```bash 26 | Would you like to display the user(s) to be imported / synchronized? (yes/no) [no]: 27 | > y 28 | ``` 29 | 30 | Confirming the display of users to will show a table of users that will be imported: 31 | 32 | ```bash 33 | +------------------------------+----------------------+----------------------------------------------+ 34 | | Name | Account Name | UPN | 35 | +------------------------------+----------------------+----------------------------------------------+ 36 | | John Doe | johndoe | johndoe@email.com | 37 | | Jane Doe | janedoe | janedoe@email.com | 38 | +------------------------------+----------------------+----------------------------------------------+ 39 | ``` 40 | 41 | After it has displayed all users, you will then be asked: 42 | 43 | ```bash 44 | Would you like these users to be imported / synchronized? (yes/no) [no]: 45 | > y 46 | 47 | 2/2 [============================] 100% 48 | 49 | Successfully imported / synchronized 2 user(s). 50 | ``` 51 | 52 | ## Scheduling the Command 53 | 54 | To run the import as a scheduled job, place the following in your `app/Console/Kernel.php` in the command scheduler: 55 | 56 | ```php 57 | /** 58 | * Define the application's command schedule. 59 | * 60 | * @param \Illuminate\Console\Scheduling\Schedule $schedule 61 | * 62 | * @return void 63 | */ 64 | protected function schedule(Schedule $schedule) 65 | { 66 | // Import LDAP users hourly. 67 | $schedule->command('adldap:import', [ 68 | '--no-interaction', 69 | '--restore', 70 | '--delete', 71 | '--filter' => '(objectclass=user)', 72 | ])->hourly(); 73 | } 74 | ``` 75 | 76 | The above scheduled import command will: 77 | 78 | - Run without interaction and import new users as well as synchronize already imported users 79 | - Restore user models who have been re-activated in your LDAP directory (if you're using [SoftDeletes](https://laravel.com/docs/5.7/eloquent#soft-deleting)) 80 | - Soft-Delete user models who have been deactived in your LDAP directory (if you're using [SoftDeletes](https://laravel.com/docs/5.7/eloquent#soft-deleting)) 81 | - Only import users that have an `objectclass` equal to `user` 82 | 83 | ### Importing a Single User 84 | 85 | To import a single user, insert one of their attributes and Adldap2 will try to locate the user for you: 86 | 87 | ```bash 88 | php artisan adldap:import jdoe@email.com 89 | 90 | Found user 'John Doe'. 91 | ``` 92 | 93 | ## Import Scope 94 | 95 | > **Note**: This feature was added in v6.0.2. 96 | 97 | To customize the query that locates the LDAP users local database model, you may 98 | use the `useScope` method on the `Import` command in your `AppServiceProvider`: 99 | 100 | ```php 101 | use App\Scopes\LdapUserImportScope; 102 | use Adldap\Laravel\Commands\Import; 103 | 104 | public function boot() 105 | { 106 | Import::useScope(LdapUserImportScope::class); 107 | } 108 | ``` 109 | 110 | The custom scope: 111 | 112 | > **Note**: It's recommended that your custom scope extend the default `UserImportScope`. 113 | > Otherwise, it must implement the `Illuminate\Database\Eloquent\Scope` interface. 114 | 115 | ```php 116 | namespace App\Scopes; 117 | 118 | use Adldap\Laravel\Facades\Resolver; 119 | use Adldap\Laravel\Commands\UserImportScope as BaseScope; 120 | 121 | class LdapUserImportScope extends BaseScope 122 | { 123 | /** 124 | * Apply the scope to a given Eloquent query builder. 125 | * 126 | * @param Builder $query 127 | * @param Model $model 128 | * 129 | * @return void 130 | */ 131 | public function apply(Builder $query, Model $model) 132 | { 133 | $query 134 | ->where(Resolver::getDatabaseIdColumn(), '=', $this->getGuid()) 135 | ->orWhere(Resolver::getDatabaseUsernameColumn(), '=', $this->getUsername()); 136 | } 137 | } 138 | ``` 139 | 140 | ## Command Options 141 | 142 | ### Filter 143 | 144 | The `--filter` (or `-f`) option allows you to enter in a raw filter in combination with your scopes inside your `config/ldap_auth.php` file: 145 | 146 | ```bash 147 | php artisan adldap:import --filter "(cn=John Doe)" 148 | 149 | Found user 'John Doe'. 150 | ``` 151 | 152 | ### Model 153 | 154 | The `--model` (or `-m`) option allows you to change the model to use for importing users. 155 | By default your configured model from your `ldap_auth.php` file will be used. 156 | 157 | ```bash 158 | php artisan adldap:import --model "\App\Models\User" 159 | ``` 160 | 161 | ### No Logging 162 | 163 | The `--no-log` option allows you to disable logging during the command. 164 | 165 | By default, this is enabled. 166 | 167 | ```bash 168 | php artisan adldap:import --no-log 169 | ``` 170 | 171 | ### Delete 172 | 173 | The `--delete` (or `-d`) option allows you to soft-delete deactivated LDAP users. No users will 174 | be deleted if your User model does not have soft-deletes enabled. 175 | 176 | ```bash 177 | php artisan adldap:import --delete 178 | ``` 179 | 180 | ### Restore 181 | 182 | The `--restore` (or `-r`) option allows you to restore soft-deleted re-activated LDAP users. 183 | 184 | ```bash 185 | php artisan adldap:import --restore 186 | ``` 187 | 188 | > **Note**: Usually the `--restore` and `--delete` options are used in tandem to allow full synchronization. 189 | 190 | ### No Interaction 191 | 192 | To run the import command via a schedule, use the `--no-interaction` flag: 193 | 194 | ```php 195 | php artisan adldap:import --no-interaction 196 | ``` 197 | 198 | Users will be imported automatically with no prompts. 199 | 200 | You can also call the command from the Laravel Scheduler, or other commands: 201 | 202 | ```php 203 | // Importing one user 204 | $schedule->command('adldap:import sbauman', ['--no-interaction']) 205 | ->everyMinute(); 206 | ``` 207 | 208 | ```php 209 | // Importing all users 210 | $schedule->command('adldap:import', ['--no-interaction']) 211 | ->everyMinute(); 212 | ``` 213 | 214 | ```php 215 | // Importing users with a filter 216 | $dn = 'CN=Accounting,OU=SecurityGroups,DC=Acme,DC=Org'; 217 | 218 | $filter = sprintf('(memberof:1.2.840.113556.1.4.1941:=%s)', $dn); 219 | 220 | $schedule->command('adldap:import', ['--no-interaction', '--filter' => $filter]) 221 | ->everyMinute(); 222 | ``` 223 | 224 | ## Tips 225 | 226 | - Users who already exist inside your database will be updated with your configured `sync_attributes` 227 | - Users are never deleted from the import command, you will need to delete users regularly through your model 228 | - Successfully imported (new) users are reported in your log files with: 229 | - `[2016-06-29 14:51:51] local.INFO: Imported user johndoe` 230 | - Unsuccessful imported users are also reported in your log files, with the message of the exception: 231 | - `[2016-06-29 14:51:51] local.ERROR: Unable to import user janedoe. SQLSTATE[23000]: Integrity constraint violation: 1048` 232 | - Specifying a username uses ambiguous naming resolution, so you're able to specify attributes other than their username, such as their email (`php artisan adldap:import jdoe@mail.com`). 233 | - If you have a password mutator (setter) on your User model, it will not override it. This way, you can hash the random 16 characters any way you please. 234 | 235 | -------------------------------------------------------------------------------- /docs/auth/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | > **Note**: If you're using Laravel 6, you must publish Laravel's auth 4 | > scaffolding by running the following commands before continuing on: 5 | > 6 | > ```bash 7 | > composer require laravel/ui --dev 8 | > 9 | > php artisan ui vue --auth 10 | > ``` 11 | 12 | To start configuring the authentication driver, you will need 13 | to publish the configuration file using the command below: 14 | 15 | ```bash 16 | php artisan vendor:publish --provider "Adldap\Laravel\AdldapAuthServiceProvider" 17 | ``` 18 | 19 | Then, open your `config/auth.php` configuration file and change the `driver` 20 | value inside the `users` authentication provider to `ldap`: 21 | 22 | ```php 23 | 'providers' => [ 24 | 'users' => [ 25 | 'driver' => 'ldap', // Changed from 'eloquent' 26 | 'model' => App\User::class, 27 | ], 28 | ], 29 | ``` 30 | 31 | > **Tip**: Now that you've enabled LDAP authentication, you may want to turn off some of 32 | > Laravel's authorization routes such as password resets, registration, and email 33 | > verification. 34 | > 35 | > You can do so in your `routes/web.php` file via: 36 | > 37 | > ```php 38 | > Auth::routes([ 39 | > 'reset' => false, 40 | > 'verify' => false, 41 | > 'register' => false, 42 | > ]); 43 | > ``` 44 | 45 | Now that you've completed the basic installation, let's move along to the [setup guide](auth/setup.md). 46 | -------------------------------------------------------------------------------- /docs/auth/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The Adldap2 Laravel auth driver allows you to seamlessly authenticate LDAP users into your Laravel application. 4 | 5 | There are two primary ways of authenticating LDAP users: 6 | 7 | - Authenticate and synchronize LDAP users into your local applications database: 8 | 9 | This allows you to attach data to users as you would in a traditional application. 10 | 11 | Calling `Auth::user()` returns your configured Eloquent model (ex. `App\User`) of the LDAP user. 12 | 13 | - Authenticate without keeping a database record for users 14 | 15 | This allows you to have temporary users. 16 | 17 | Calling `Auth::user()` returns the actual LDAP users model (ex. `Adldap\Models\User`). 18 | 19 | We'll get into each of these methods and how to implement them, but first, lets go through the [installation guide](auth/installation.md). 20 | 21 | ## Quick Start - From Scratch 22 | 23 | Here is a step by step guide for configuring Adldap2-Laravel (and its auth driver) with a fresh new laravel project. This guide assumes you have knowledge working with: 24 | 25 | - Laravel 26 | - The LDAP Protocol 27 | - Your LDAP distro (ActiveDirectory, OpenLDAP, FreeIPA) 28 | - Command line tools (such as Composer and Laravel's Artisan). 29 | 30 | This guide was created with the help of [@st-claude](https://github.com/st-claude) and other awesome contributors. 31 | 32 | 1. Create a new laravel project by running the command: 33 | - `laravel new my-ldap-app` 34 | 35 | Or (if you don't have the [Laravel Installer](https://laravel.com/docs/5.7#installation)) 36 | 37 | - `composer create-project --prefer-dist laravel/laravel my-app`. 38 | 39 | 2. Run the following command to install Adldap2-Laravel: 40 | 41 | - `composer require adldap2/adldap2-laravel` 42 | 43 | 3. Create a new database in your desired database interface (such as PhpMyAdmin, MySQL Workbench, command line etc.) 44 | 45 | 4. Enter your database details and credentials inside the `.env` file located in your project root directory (if there is not one there, rename the `.env.example` to `.env`). 46 | 47 | 5. If you're using username's to login users **instead** of their emails, you will need to change 48 | the default `email` column in `database/migrations/2014_10_12_000000_create_users_table.php`. 49 | 50 | ```php 51 | // database/migrations/2014_10_12_000000_create_users_table.php 52 | 53 | Schema::create('users', function (Blueprint $table) { 54 | $table->increments('id'); 55 | $table->string('name'); 56 | 57 | // From: 58 | $table->string('email')->unique(); 59 | 60 | // To: 61 | $table->string('username')->unique(); 62 | 63 | $table->string('password'); 64 | $table->rememberToken(); 65 | $table->timestamps(); 66 | }); 67 | ``` 68 | 69 | 6. Now run `php artisan migrate`. 70 | 71 | 7. Insert the following service providers in your `config/app.php` file (in the `providers` array): 72 | 73 | > **Note**: This step is only required for Laravel 5.0 - 5.4. 74 | > They are registered automatically in Laravel 5.5. 75 | 76 | ```php 77 | Adldap\Laravel\AdldapServiceProvider::class, 78 | Adldap\Laravel\AdldapAuthServiceProvider::class, 79 | ``` 80 | 81 | 8. Now, insert the facade into your `config/app.php` file (in the `aliases` array): 82 | 83 | ```php 84 | 'Adldap' => Adldap\Laravel\Facades\Adldap::class, 85 | ``` 86 | 87 | > **Note**: Insertion of this alias in your `app.php` file isn't necessary unless you're planning on utilizing it. 88 | 89 | 9. Now run `php artisan vendor:publish` in your root project directory to publish Adldap2's configuration files. 90 | 91 | * Two files will be published inside your `config` folder, `ldap.php` and `ldap_auth.php`. 92 | 93 | 10. Modify the `config/ldap.php` and `config/ldap_auth.php` files for your LDAP server configuration. 94 | 95 | 11. Run the command `php artisan make:auth` to scaffold login controllers and routes. 96 | 97 | 12. If you require logging in by another attribute, such as a username instead of email follow 98 | the process below for your Laravel version. Otherwise ignore this step. 99 | 100 | **Laravel <= 5.2** 101 | 102 | Inside the generated `app/Http/Controllers/Auth/AuthController.php`, you'll need to add the `protected $username` property if you're logging in users by username. 103 | 104 | ```php 105 | class AuthController extends Controller 106 | { 107 | protected $username = 'username'; 108 | ``` 109 | 110 | **Laravel > 5.3** 111 | 112 | Inside the generated `app/Http/Controllers/Auth/LoginController.php`, you'll need to add the public method `username()`: 113 | 114 | ```php 115 | public function username() 116 | { 117 | return 'username'; 118 | } 119 | ``` 120 | 121 | 13. Now insert a new auth driver inside your `config/auth.php` file: 122 | 123 | ```php 124 | 'providers' => [ 125 | 'users' => [ 126 | 'driver' => 'ldap', // Was 'eloquent'. 127 | 'model' => App\User::class, 128 | ], 129 | ], 130 | ``` 131 | 132 | 14. Inside your `resources/views/auth/login.blade.php` file, if you're requiring the user logging in by username, you'll 133 | need to modify the HTML input to `username` instead of `email`. Ignore this step otherwise. 134 | 135 | From: 136 | ```html 137 | 138 | ``` 139 | 140 | To: 141 | 142 | ```html 143 | 144 | ``` 145 | 146 | 15. You should now be able to login to your Laravel application using LDAP authentication! 147 | 148 | If you check out your database in your `users` table, you'll see that your LDAP account was synchronized to a local user account. 149 | 150 | This means that you can attach data regularly to this user as you would with standard Laravel authentication. 151 | 152 | If you're having issues, and you're unable to authenticate LDAP users, please check your configuration settings inside the `ldap.php` and `ldap_auth.php` files as these directly impact your applications ability to authenticate. 153 | 154 | 16. Congratulations, you're awesome. 155 | -------------------------------------------------------------------------------- /docs/auth/middleware.md: -------------------------------------------------------------------------------- 1 | # Middleware 2 | 3 | SSO authentication allows you to authenticate your domain users automatically in your application by 4 | the pre-populated `$_SERVER['AUTH_USER']` (or `$_SERVER['REMOTE_USER']`) that is filled when 5 | users visit your site when SSO is enabled on your server. This is 6 | configurable in your `ldap_auth.php`configuration file in the `identifiers` array. 7 | 8 | > **Requirements**: This feature assumes that you have enabled `Windows Authentication` in IIS, or have enabled it 9 | > in some other means with Apache. Adldap2 does not set this up for you. To enable Windows Authentication, visit: 10 | > https://www.iis.net/configreference/system.webserver/security/authentication/windowsauthentication/providers/add 11 | 12 | > **Note**: The WindowsAuthenticate middleware utilizes the `scopes` inside your `config/ldap.php` file. 13 | > A user may successfully authenticate against your LDAP server when visiting your site, but 14 | > depending on your scopes, may not be imported or logged in. 15 | 16 | To use the middleware, insert it on your middleware stack inside your `app/Http/Kernel.php` file: 17 | 18 | ```php 19 | protected $middlewareGroups = [ 20 | 'web' => [ 21 | Middleware\EncryptCookies::class, 22 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 23 | \Illuminate\Session\Middleware\StartSession::class, 24 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 25 | Middleware\VerifyCsrfToken::class, 26 | \Adldap\Laravel\Middleware\WindowsAuthenticate::class, // Inserted here. 27 | ], 28 | ]; 29 | ``` 30 | 31 | Now when you visit your site, a user account will be created (if one does not exist already) 32 | with a random 16 character string password and then automatically logged in. Neat huh? 33 | 34 | ## Configuration 35 | 36 | You can configure the attributes users are logged in by in your configuration: 37 | 38 | ```php 39 | 'usernames' => [ 40 | //..// 41 | 42 | 'windows' => [ 43 | 'locate_users_by' => 'samaccountname', 44 | 'server_key' => 'AUTH_USER', 45 | ], 46 | ], 47 | ``` 48 | 49 | If a user is logged into a domain joined computer and is visiting your website with windows 50 | authentication enabled, IIS will set the PHP server variable `AUTH_USER`. This variable 51 | is usually equal to the currently logged in users `samaccountname`. 52 | 53 | The configuration array represents this mapping. The WindowsAuthenticate middleware will 54 | check if the server variable is set, and try to locate the user in your LDAP server 55 | by their `samaccountname`. 56 | -------------------------------------------------------------------------------- /docs/auth/model-binding.md: -------------------------------------------------------------------------------- 1 | # Model Binding 2 | 3 | Model binding allows you to attach the users LDAP model to their Eloquent 4 | model so their LDAP data is available on every request automatically. 5 | 6 | > **Note**: Before we begin, enabling this option will perform a single query on your LDAP server for a logged 7 | > in user **per request**. Eloquent already does this for authentication, however 8 | > this could lead to slightly longer load times (depending on your LDAP 9 | > server and network speed of course). 10 | 11 | To begin, insert the `Adldap\Laravel\Traits\HasLdapUser` trait onto your `User` model: 12 | 13 | ```php 14 | namespace App; 15 | 16 | use Adldap\Laravel\Traits\HasLdapUser; 17 | use Illuminate\Database\Eloquent\SoftDeletes; 18 | use Illuminate\Foundation\Auth\User as Authenticatable; 19 | 20 | class User extends Authenticatable 21 | { 22 | use SoftDeletes, HasLdapUser; 23 | ``` 24 | 25 | Now, after you've authenticated a user (with the `ldap` auth driver), 26 | their LDAP model will be available on their `User` model: 27 | 28 | ```php 29 | if (Auth::attempt($credentials)) { 30 | $user = Auth::user(); 31 | 32 | var_dump($user); // Returns instance of App\User; 33 | 34 | var_dump($user->ldap); // Returns instance of Adldap\Models\User; 35 | 36 | // Examples: 37 | 38 | $user->ldap->getGroups(); 39 | 40 | $user->ldap->getCommonName(); 41 | 42 | $user->ldap->getConvertedSid(); 43 | } 44 | ``` -------------------------------------------------------------------------------- /docs/auth/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | To test that your configured LDAP connection is being authenticated against, you can utilize the `Adldap\Laravel\Facades\Resolver` facade. 4 | 5 | Using the facade, you can mock certain methods to return mock LDAP users 6 | and pass or deny authentication to test different scenarios. 7 | 8 | ```php 9 | make()->user($attributes); 36 | } 37 | 38 | /** 39 | * A basic test example. 40 | * 41 | * @return void 42 | */ 43 | public function test_ldap_authentication_works() 44 | { 45 | $credentials = ['email' => 'jdoe@email.com', 'password' => '12345']; 46 | 47 | $user = $this->makeLdapUser([ 48 | 'objectguid' => [$this->faker->uuid], 49 | 'cn' => ['John Doe'], 50 | 'userprincipalname' => ['jdoe@email.com'], 51 | ]); 52 | 53 | Resolver::shouldReceive('byCredentials')->once()->with($credentials)->andReturn($user) 54 | ->shouldReceive('getDatabaseIdColumn')->twice()->andReturn('objectguid') 55 | ->shouldReceive('getDatabaseUsernameColumn')->once()->andReturn('email') 56 | ->shouldReceive('getLdapDiscoveryAttribute')->once()->andReturn('userprincipalname') 57 | ->shouldReceive('authenticate')->once()->andReturn(true); 58 | 59 | $this->post(route('login'), $credentials)->assertRedirect('/dashboard'); 60 | 61 | $this->assertInstanceOf(User::class, Auth::user()); 62 | } 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adldap2-Laravel Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | Adldap2-Laravel requires the following: 4 | 5 | - Laravel 5.5 6 | - PHP 7.1 or greater 7 | - PHP LDAP extension enabled 8 | - An LDAP Server 9 | 10 | # Composer 11 | 12 | Run the following command in the root of your project: 13 | 14 | ```bash 15 | composer require adldap2/adldap2-laravel 16 | ``` 17 | 18 | > **Note**: If you are using laravel 5.5 or higher you can skip the service provider 19 | > and facade registration and continue with publishing the configuration file. 20 | 21 | Once finished, insert the service provider in your `config/app.php` file: 22 | 23 | ```php 24 | Adldap\Laravel\AdldapServiceProvider::class, 25 | ``` 26 | 27 | Then insert the facade alias (if you're going to use it): 28 | 29 | ```php 30 | 'Adldap' => Adldap\Laravel\Facades\Adldap::class 31 | ``` 32 | 33 | Finally, publish the `ldap.php` configuration file by running: 34 | 35 | ```bash 36 | php artisan vendor:publish --provider "Adldap\Laravel\AdldapServiceProvider" 37 | ``` 38 | 39 | Now you're all set! You're ready to move onto [setup](setup.md). -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## What is Adldap2-Laravel? 4 | 5 | Adldap2-Laravel is an extension to the core [Adldap2](https://github.com/Adldap2/Adldap2) package. 6 | 7 | This package allows you to: 8 | 9 | 1. Easily configure and manage multiple LDAP connections at once 10 | 2. Authenticate LDAP users into your Laravel application 11 | 3. Import / Synchronize LDAP users into your database and easily keep them up to date with changes in your directory 12 | 4. Search your LDAP directory with a fluent and easy to use query builder 13 | 5. Create / Update / Delete LDAP entities with ease 14 | 6. And more 15 | 16 | ## Quick Start 17 | 18 | Install Adldap2-Laravel via [composer](https://getcomposer.org/) using the command: 19 | 20 | ```bash 21 | composer require adldap2/adldap2-laravel 22 | ``` 23 | 24 | Publish the configuration file using: 25 | 26 | ```bash 27 | php artisan vendor:publish --provider="Adldap\Laravel\AdldapServiceProvider" 28 | ``` 29 | 30 | Configure your LDAP connection in the published `ldap.php` file. 31 | 32 | Then, use the `Adldap\Laravel\Facades\Adldap` facade: 33 | 34 | ```php 35 | use Adldap\Laravel\Facades\Adldap; 36 | 37 | // Finding a user: 38 | $user = Adldap::search()->users()->find('john doe'); 39 | 40 | // Searching for a user: 41 | $search = Adldap::search()->where('cn', '=', 'John Doe')->get(); 42 | 43 | // Running an operation under a different connection: 44 | $users = Adldap::getProvider('other-connection')->search()->users()->get(); 45 | 46 | // Creating a user: 47 | $user = Adldap::make()->user([ 48 | 'cn' => 'John Doe', 49 | ]); 50 | 51 | // Modifying Attributes: 52 | $user->cn = 'Jane Doe'; 53 | 54 | // Saving a user: 55 | $user->save(); 56 | ``` 57 | 58 | **Or** inject the `Adldap\AdldapInterface`: 59 | 60 | ```php 61 | use Adldap\AdldapInterface; 62 | 63 | class UserController extends Controller 64 | { 65 | /** 66 | * @var Adldap 67 | */ 68 | protected $ldap; 69 | 70 | /** 71 | * Constructor. 72 | * 73 | * @param AdldapInterface $adldap 74 | */ 75 | public function __construct(AdldapInterface $ldap) 76 | { 77 | $this->ldap = $ldap; 78 | } 79 | 80 | /** 81 | * Displays the all LDAP users. 82 | * 83 | * @return \Illuminate\View\View 84 | */ 85 | public function index() 86 | { 87 | $users = $this->ldap->search()->users()->get(); 88 | 89 | return view('users.index', compact('users')); 90 | } 91 | 92 | /** 93 | * Displays the specified LDAP user. 94 | * 95 | * @return \Illuminate\View\View 96 | */ 97 | public function show($id) 98 | { 99 | $user = $this->ldap->search()->findByGuid($id); 100 | 101 | return view('users.show', compact('user')); 102 | } 103 | } 104 | ``` 105 | 106 | ## Versioning 107 | 108 | Adldap2-Laravel is versioned under the [Semantic Versioning](http://semver.org/) guidelines as much as possible. 109 | 110 | Releases will be numbered with the following format: 111 | 112 | `..` 113 | 114 | And constructed with the following guidelines: 115 | 116 | * Breaking backward compatibility bumps the major and resets the minor and patch. 117 | * New additions without breaking backward compatibility bumps the minor and resets the patch. 118 | * Bug fixes and misc changes bumps the patch. 119 | 120 | Minor versions are not maintained individually, and you're encouraged to upgrade through to the next minor version. 121 | 122 | Major versions are maintained individually through separate branches. 123 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Configuration 4 | 5 | Upon publishing your `ldap.php` configuration, you'll see an array named `connections`. This 6 | array contains a key value pair for each LDAP connection you're looking to configure. 7 | 8 | Each connection you configure should be separate domains. Only one connection is necessary 9 | when using multiple LDAP servers on the same domain. 10 | 11 | ### Connection Name 12 | 13 | The `default` key is your LDAP connections name. This is used as an identifier when connecting. 14 | 15 | Usually this is set to your domain name. For example: 16 | 17 | ```php 18 | 'connections' => [ 19 | 'corp.acme.org' => [ 20 | '...', 21 | ], 22 | ], 23 | ``` 24 | 25 | You may change this to whatever name you prefer. 26 | 27 | ### Auto Connect 28 | 29 | The `auto_connect` configuration option determines whether Adldap2-Laravel will try to bind to your 30 | LDAP server automatically using your configured credentials when calling the `Adldap` 31 | facade or injecting the `AdldapInterface` interface. 32 | 33 | For the example below, notice how we don't have to connect manually and we can assume connectivity: 34 | 35 | ```php 36 | use Adldap\AdldapInterface; 37 | 38 | public class UserController extends Controller 39 | { 40 | public function index(AdldapInterface $ldap) 41 | { 42 | return view('users.index', [ 43 | 'users' => $ldap->search()->users()->get(); 44 | ]); 45 | } 46 | } 47 | ``` 48 | 49 | If this is set to `false`, you **must** connect manually before running operations on your server. 50 | Otherwise, you will receive an exception upon performing operations. 51 | 52 | ### Settings 53 | 54 | The `settings` option contains a configuration array of your LDAP server connection. 55 | 56 | Please view the core [Aldap2 Configuration Guide](https://adldap2.github.io/Adldap2/#/setup?id=options) 57 | for definitions on each option and its meaning. 58 | 59 | Once you've done so, you're ready to move to the [usage guide](usage.md). 60 | -------------------------------------------------------------------------------- /docs/tutorials/basics.md: -------------------------------------------------------------------------------- 1 | # The Basics 2 | 3 | Lets get down to the basics. This guide will help you get a quick understanding of 4 | using Adldap2 and cover some use cases you might want to learn how to 5 | perform before trying to work it out on your own. 6 | 7 | ## Searching 8 | 9 | ### Querying for your Base DN 10 | 11 | If you're not sure what your base distinguished name should be, you can use the query 12 | builder to locate it for you if you're making a successful connection to the server: 13 | 14 | ```php 15 | $base = Adldap::search()->findBaseDn(); 16 | 17 | echo $base; // Returns 'dc=corp,dc=acme,dc=org' 18 | ``` 19 | 20 | ### Querying for Enabled / Disabled Users 21 | 22 | To locate enabled / disabled users in your directory, call the `whereEnabled()` 23 | and `whereDisabled()` methods on a query: 24 | 25 | ```php 26 | $enabledUsers = Adldap:search()->users()->whereEnabled()->get(); 27 | 28 | $disabledUsers = Adldap:search()->users()->whereDisabled()->get(); 29 | ``` 30 | 31 | ### Querying for Group Membership 32 | 33 | To locate records in your directory that are apart of a group, use the `whereMemberOf()` query method: 34 | 35 | ```php 36 | // First, locate the group we want to retrieve the members for: 37 | $accounting = Adldap::search()->groups()->find('Accounting'); 38 | 39 | // Retrieve the members that belong to the above group. 40 | $results = Adldap::search()->whereMemberOf($accounting)->get(); 41 | 42 | // Iterate through the results: 43 | foreach ($results as $model) { 44 | $model->getCommonName(); // etc. 45 | } 46 | ``` 47 | 48 | ### Escaping Input Manually 49 | 50 | If you'd like to execute raw filters, it's best practice to escape any input you receive from a user. 51 | 52 | You can do this in a couple ways, so use whichever feels best to you. 53 | 54 | Each escape method below will escape all characters inputted unless an **ignore** parameter or **flag** 55 | parameter have been given (such as `LDAP_ESCAPE_FITLER` or `LDAP_ESCAPE_DN`). 56 | 57 | ```php 58 | // Escaping using the query builder: 59 | $escaped = Adldap::search()->escape($input, $ignore = '', $flags = 0); 60 | 61 | // Escaping using the `Utilities` class: 62 | $escaped = \Adldap\Utilities::escape($input, $ignore = '', $flags = 0); 63 | 64 | // Escaping with the native PHP: 65 | $escaped = ldap_escape($input, $ignore = '', $flags = 0); 66 | 67 | $rawFilter = `(samaccountname=$escaped)` 68 | 69 | $results = Adldap::search()->rawFilter($rawFilter)->get(); 70 | ``` 71 | 72 | ## Models 73 | 74 | ### Creating 75 | 76 | ### Updating 77 | 78 | ### Deleting 79 | 80 | ### Moving 81 | 82 | ### Renaming 83 | -------------------------------------------------------------------------------- /docs/tutorials/users.md: -------------------------------------------------------------------------------- 1 | # User Tutorials 2 | 3 | > **Notice**: These tutorials have been created using ActiveDirectory. 4 | > Some tutorials may not relate to your LDAP distribution. 5 | 6 | > **Note**: You cannot create or modify user passwords without 7 | > connecting to your LDAP server via SSL or TLS. 8 | 9 | ## Creating Users 10 | 11 | To begin, creating a user is actually quite simple, as it only requires a Common Name: 12 | 13 | ```php 14 | $user = Adldap::make()->user([ 15 | 'cn' => 'John Doe', 16 | ]); 17 | 18 | $user->save(); 19 | ``` 20 | 21 | If you'd like to provide more attributes, simply add more: 22 | 23 | ```php 24 | $user = Adldap::make()->user([ 25 | 'cn' => 'John Doe', 26 | 'sn' => 'Doe', 27 | 'givenname' => 'John', 28 | 'department' => 'Accounting' 29 | ]); 30 | 31 | $user->save(); 32 | ``` 33 | 34 | If you don't provide a Distinguished Name to the user during creation, one will be set for you automatically 35 | by taking your configured `base_dn` and using the users Common Name you give them: 36 | 37 | ```php 38 | $user = Adldap::make()->user([ 39 | 'cn' => 'John Doe', 40 | ]); 41 | 42 | $user->save(); // Creates a user with the DN: 'cn=John Doe,dc=acme,dc=org' 43 | ``` 44 | 45 | You can provide a `dn` attribute to set the users Distinguished Name you would like to use for creation: 46 | 47 | ```php 48 | $user = Adldap::make()->user([ 49 | 'cn' => 'John Doe', 50 | 'dn' => 'cn=John Doe,ou=Users,dc=acme,dc=com' 51 | ]); 52 | 53 | $user->save(); 54 | ``` 55 | 56 | All users created in your directory will be disabled by default. How do we enable these users upon creation and set thier password? 57 | 58 | What we can use is the `Adldap\Models\Attributes\AccountControl` attribute class and the `userPassword` attribute. 59 | 60 | ```php 61 | // Encode the users password. 62 | $password = Adldap\Utilies::encodePassword('super-secret'); 63 | 64 | // Create a new AccountControl object. 65 | $uac = new Adldap\Models\Attributes\AccountControl(); 66 | 67 | // Set the UAC value to '512'. 68 | $uac->accountIsNormal(); 69 | 70 | $user = Adldap::make()->user([ 71 | 'cn' => 'John Doe', 72 | 'dn' => 'cn=John Doe,ou=Users,dc=acme,dc=com' 73 | 'userPassword' => $password, 74 | 'userAccountControl' => $uac->getValue(), 75 | ]); 76 | 77 | $user->save(); 78 | ``` 79 | 80 | You can also fluently create accounts using setter methods if you'd prefer: 81 | 82 | > **Note**: There are some conveniences that come with using the setter methods. 83 | > Notice how you don't have to encode the password using the `setPassword()` 84 | > method or call `getValue()` when setting the users account control. 85 | 86 | ```php 87 | // Create a new AccountControl object. 88 | $uac = new Adldap\Models\Attributes\AccountControl(); 89 | 90 | $uac->accountIsNormal(); 91 | 92 | $user = Adldap::make()->user(); 93 | 94 | $user 95 | ->setCommonName('John Doe') 96 | ->setDn('cn=John Doe,ou=Users,dc=acme,dc=com') 97 | ->setPassword('super-secret') 98 | ->setUserAccountControl($uac); 99 | 100 | $user->save(); 101 | ``` 102 | 103 | ## Modifying Users 104 | 105 | You can modify users in a variety of ways. Each way will be shown below. 106 | Use whichever ways you prefer readable and most clear to you. 107 | 108 | To modify users, you simply modify their attributes using dynamic properties and `save()` them: 109 | 110 | ```php 111 | $jdoe = Adldap::search()->users()->find('jdoe'); 112 | 113 | $uac = new Adldap\Models\Attributes\AccountControl(); 114 | 115 | $uac->accountIsNormal(); 116 | 117 | $jdoe->deparment = 'Accounting'; 118 | $jdoe->telephoneNumber = '555 555-5555'; 119 | $jdoe->mobile = '555 444-4444'; 120 | $jdoe->userAccountControl = $uac->getValue(); 121 | 122 | $jdoe->save(); 123 | ``` 124 | 125 | You can also use 'setter' methods to perform the same task. 126 | 127 | ```php 128 | $jdoe = Adldap::search()->users()->find('jdoe'); 129 | 130 | $uac = new Adldap\Models\Attributes\AccountControl(); 131 | 132 | $uac->accountIsNormal(); 133 | 134 | $jdoe 135 | ->setDepartment('Accounting'); 136 | ->setTelephoneNumber('555 555-5555') 137 | ->setMobileNumber('555 555-5555') 138 | ->setUserAccountControl($uac); 139 | 140 | $jdoe->save(); 141 | ``` 142 | 143 | Using setter methods offer a little bit of benefit, for example you can see above that 144 | `$uac->getValue()` does not need to be called as the `setUserAccountControl()` method 145 | will automatically convert an `AccountControl` object to its integer value. 146 | 147 | Setter methods are also chainable (if you prefer that syntax). 148 | 149 | ## Deleting Users 150 | 151 | As any other returned model in Adldap2, you can call the `delete()` method 152 | to delete a user from your directory: 153 | 154 | ```php 155 | $user = Adldap::search()->find('jdoe'); 156 | 157 | $user->delete(); 158 | ``` 159 | 160 | Once you `delete()` a user (successfully), the `exists` property on their model is set to `false`: 161 | 162 | ```php 163 | $user = Adldap::search()->find('jdoe'); 164 | 165 | $user->delete(); 166 | 167 | var_dump($user->exists); // Returns 'bool(false)' 168 | ``` 169 | -------------------------------------------------------------------------------- /docs/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Upgrading from 5.* to 6.* 4 | 5 | **Estimated Upgrade Time: 1 hour** 6 | 7 | ### Minimum Requirements 8 | 9 | Adldap2-Laravel now requires a minimum of Laravel 5.5, as all previous versions are now out of their respective support windows. 10 | 11 | If you require using an earlier version of Laravel, please use Adldap2-Laravel v5.0. 12 | 13 | ### Configuration 14 | 15 | It is recommended to re-publish both of your `ldap.php` and `ldap_auth.php` 16 | files to ensure you have all of the updated configuration keys. 17 | 18 | You can do so by deleting your `ldap.php` and `ldap_auth.php` files and then running: 19 | 20 | - `php artisan vendor:publish --provider=Adldap\Laravel\AdldapServiceProvider` 21 | - `php artisan vendor:publish --provider=Adldap\Laravel\AdldapAuthServiceProvider` 22 | 23 | #### Quick Changes View 24 | 25 | Here's a quick overview of the configuration changes made in their respective files: 26 | 27 | ```php 28 | // ldap.php 29 | 30 | // v5.0 31 | // Non-existent 32 | 33 | // v6.0 34 | 'logging' => env('LDAP_LOGGING', true), 35 | ``` 36 | 37 | ```php 38 | // ldap_auth.php 39 | 40 | // v5.0 41 | 'usernames' => [ 42 | 43 | 'ldap' => [ 44 | 'discover' => 'userprincipalname', 45 | 'authenticate' => 'distinguishedname', 46 | ], 47 | 48 | 'eloquent' => 'email', 49 | 50 | 'windows' => [ 51 | 'discover' => 'samaccountname', 52 | 'key' => 'AUTH_USER', 53 | ], 54 | 55 | ], 56 | 57 | // v6.0 58 | 'model' => App\User::class, 59 | 60 | 'identifiers' => [ 61 | 62 | 'ldap' => [ 63 | 'locate_users_by' => 'userprincipalname', 64 | 'bind_users_by' => 'distinguishedname', 65 | ], 66 | 67 | 'database' => [ 68 | 'guid_column' => 'objectguid', 69 | 'username_column' => 'email', 70 | ], 71 | 72 | 'windows' => [ 73 | 'locate_users_by' => 'samaccountname', 74 | 'server_key' => 'AUTH_USER', 75 | ], 76 | 77 | ] 78 | ``` 79 | 80 | #### Authentication 81 | 82 | ##### Object GUID Database Column 83 | 84 | When using the `DatabaseUserProvider`, you must now create a database column to 85 | store users `objectguid`. This allows usernames to change in your directory 86 | and synchronize properly in your database. This also allows you to use 87 | multiple LDAP directories / domains in your application. 88 | 89 | This column is configurable via the `guid_column` located in the `database` configuration array: 90 | 91 | ```php 92 | 'database' => [ 93 | 94 | 'guid_column' => 'objectguid', 95 | 96 | // 97 | ``` 98 | 99 | If you're starting from scratch, simply add the `objectguid` column (or whichever column you've configured) to your `users` migration file: 100 | 101 | ```php 102 | Schema::create('users', function (Blueprint $table) { 103 | $table->increments('id'); 104 | $table->string('objectguid')->unique()->nullable(); // Added here. 105 | $table->string('name'); 106 | $table->string('email')->unique(); 107 | $table->timestamp('email_verified_at')->nullable(); 108 | $table->string('password'); 109 | $table->rememberToken(); 110 | $table->timestamps(); 111 | }); 112 | ``` 113 | 114 | Otherwise if you're upgrading from v5, make another migration and add the column to your `users` table. 115 | 116 | Ex. `php artisan make:migration add_objectguid_column` 117 | 118 | ```php 119 | Schema::table('users', function (Blueprint $table) { 120 | $table->string('objectguid')->unique()->nullable()->after('id'); 121 | }); 122 | ``` 123 | 124 | You can learn more about this configuration option [here](auth/setup.md#guid-column). 125 | 126 | ##### Username Database Column 127 | 128 | The `database.username_column` option was renamed from `eloquent` to more directly indicate what it is used for. 129 | 130 | Set this option to your users database username column so users are correctly located from your database. 131 | 132 | ##### LDAP Discover and Authenticate 133 | 134 | The `ldap.discover` and `ldap.authenticate` options have been renamed to `ldap.locate_users_by` and `ldap.bind_user_by` respectively. 135 | 136 | They were renamed to more directly indicate what they are used for. 137 | 138 | ##### Windows Discover and Key 139 | 140 | The `windows.discover` and `windows.key` options were renamed to `windows.locate_users_by` and `windows.server_key` to follow suit with the above change and to directly indicate what it is used for. 141 | 142 | #### LDAP 143 | 144 | ##### Logging 145 | 146 | The `logging` option has been added to automatically enable LDAP operation logging that was added in [Adldap2 v10.0](https://adldap2.github.io/Adldap2/#/logging). 147 | 148 | Simply set this to `false` if you would not like operation logging enabled. Any connections you specify in your `connections` configuration will be logged. 149 | 150 | ## Upgrading from 4.* to 5.* 151 | 152 | **Estimated Upgrade Time: 30 minutes** 153 | 154 | Functionally, you should not need to change the way you use Adldap2-Laravel. There have been no major API changes that will impact your current usage. 155 | 156 | However, there have been API changes to the core [Adldap2](https://github.com/Adldap2/Adldap2/releases/tag/v9.0.0) package. 157 | It is heavily recommended to read the release notes to see if you may be impacted. 158 | 159 | ### Requirements 160 | 161 | Adldap2-Laravel's PHP requirements has been changed. It now requires a minimum of PHP 7.1. 162 | 163 | However, Adldap2's Laravel requirements **have not** changed. You can still use all versions of Laravel 5. 164 | 165 | ### Configuration 166 | 167 | Both Adldap2's configuration files have been renamed to `ldap.php` and `ldap_auth.php` for simplicity. 168 | 169 | Simply rename `adldap.php` to `ldap.php` and `adldap_auth.php` to `ldap_auth.php`. 170 | 171 | If you'd prefer to re-publish them from scratch, here's a quick guide: 172 | 173 | 1. Delete your `config/adldap.php` file 174 | 2. Run `php artisan vendor:publish --provider="Adldap\Laravel\AdldapServiceProvider"` 175 | 176 | If you're using the Adldap2 authentication driver, repeat the same steps for its configuration: 177 | 178 | 1. Delete your `config/adldap_auth.php` file 179 | 2. Run `php artisan vendor:publish --provider="Adldap\Laravel\AdldapAuthServiceProvider"` 180 | 181 | #### Prefix and Suffix Changes 182 | 183 | The configuration options `admin_account_prefix` and `admin_account_suffix` have been removed. Simply 184 | apply a prefix and suffix to the username of the administrator account in your configuration. 185 | 186 | The `account_prefix` and `account_suffix` options now only apply to user accounts that are 187 | authenticated, not your configured administrator account. 188 | 189 | This means you will need to add your suffix or prefix onto your configured administrators username if you require it. 190 | 191 | #### Connection Settings 192 | 193 | The configuration option named `connection_settings` inside each of your configured connections in the `adldap.php` (now `ldap.php`) configuration file has been renamed to `settings` for simplicity. 194 | 195 | ### Authentication Driver 196 | 197 | The authentication driver name has been *renamed* to **ldap** instead of **adldap**. This is for the sake of simplicity. 198 | 199 | Open your `auth.php` file and rename your authentication driver to `ldap`: 200 | 201 | ```php 202 | 'users' => [ 203 | 'driver' => 'ldap', // Renamed from 'adldap' 204 | 'model' => App\User::class, 205 | ], 206 | ``` 207 | 208 | ## Upgrading From 3.* to 4.* 209 | 210 | **Estimated Upgrade Time: 1 hour** 211 | 212 | With `v4.0`, there are some significant changes to the code base. 213 | 214 | This new version utilizes the newest `v8.0` release of the underlying Adldap2 repository. 215 | 216 | Please visit the [Adldap2](https://github.com/Adldap2/Adldap2/releases/tag/v8.0.0) 217 | repository for the release notes and changes. 218 | 219 | However for this package you should only have to change your `adldap_auth.php` configuration. 220 | 221 | ### Authentication Driver 222 | 223 | LDAP connection exceptions are now caught when authentication attempts occur. 224 | 225 | These exceptions are logged to your configured logging driver so you can view the stack trace and discover issues easier. 226 | 227 | ### Configuration 228 | 229 | 1. Delete your `config/adldap_auth.php` 230 | 2. Run `php artisan vendor:publish --tag="adldap"` 231 | 3. Reconfigure auth driver in `config/adldap_auth.php` 232 | 233 | #### Usernames Array 234 | 235 | The `usernames` array has been updated with more options. 236 | 237 | You can now configure the attribute you utilize for discovering the LDAP user as well as authenticating. 238 | 239 | This will help users who use OpenLDAP and other distributions of directory servers. 240 | 241 | Each configuration option is extensively documented in the published 242 | file, so please take a moment to review it once published. 243 | 244 | This array now also contains the `windows_auth_attribute` array (shown below). 245 | 246 | ```php 247 | // v3.0 248 | 'usernames' => [ 249 | 250 | 'ldap' => 'userprincipalname', 251 | 252 | 'eloquent' => 'email', 253 | 254 | ], 255 | 256 | // v4.0 257 | 'usernames' => [ 258 | 259 | 'ldap' => [ 260 | 261 | 'discover' => 'userprincipalname', 262 | 263 | 'authenticate' => 'distinguishedname', 264 | 265 | ], 266 | 267 | 'eloquent' => 'email', 268 | 269 | 'windows' => [ 270 | 271 | 'discover' => 'samaccountname', 272 | 273 | 'key' => 'AUTH_USER', 274 | 275 | ], 276 | 277 | ], 278 | ``` 279 | 280 | #### Logging 281 | 282 | Logging has been added for authentication requests to your server. 283 | 284 | Which events are logged can be configured in your `adldap_auth.php` file. 285 | 286 | Here's an example of the information logged: 287 | 288 | ``` 289 | [2017-11-14 22:19:45] local.INFO: User 'Steve Bauman' has been successfully found for authentication. 290 | [2017-11-14 22:19:45] local.INFO: User 'Steve Bauman' is being imported. 291 | [2017-11-14 22:19:45] local.INFO: User 'Steve Bauman' is being synchronized. 292 | [2017-11-14 22:19:45] local.INFO: User 'Steve Bauman' has been successfully synchronized. 293 | [2017-11-14 22:19:45] local.INFO: User 'Steve Bauman' is authenticating with username: 'sbauman@company.org' 294 | [2017-11-14 22:19:45] local.INFO: User 'Steve Bauman' has successfully passed LDAP authentication. 295 | [2017-11-14 22:19:46] local.INFO: User 'Steve Bauman' has been successfully logged in. 296 | ``` 297 | 298 | #### Resolver 299 | 300 | The resolver configuration option has now been removed. 301 | 302 | It has been modified to utilize Laravel's Facades so you can now swap the implementation at runtime if you wish. 303 | 304 | The complete namespace for this facade is below: 305 | 306 | ``` 307 | Adldap\Laravel\Facades\Resolver 308 | ``` 309 | 310 | Usage: 311 | 312 | ```php 313 | use Adldap\Laravel\Facades\Resolver; 314 | 315 | Resolver::swap(new MyResolver()); 316 | ``` 317 | 318 | #### Importer 319 | 320 | The importer configuration option has now been removed. 321 | 322 | The importer command is bound to Laravel's IoC and can be swapped out with your own implementation if you wish. 323 | 324 | #### NoDatabaseUserProvider 325 | 326 | The `NoDatabaseUserProvider` will now locate users by their ObjectGUID instead of their ObjectSID. 327 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Adldap2-Laravel leverages the core [Adldap2](https://github.com/Adldap2/Adldap2) package. 4 | 5 | When you insert the `Adldap\Laravel\AdldapServiceProvider` into your `config/app.php`, an instance of the [Adldap\Adldap](https://adldap2.github.io/Adldap2/#/setup?id=getting-started) class is created and bound as a singleton into your application. 6 | 7 | This means, upon calling the included facade (`Adldap\Laravel\Facades\Adldap`) or interface (`Adldap\AdldapInterface`), the same instance will be returned. 8 | 9 | This is extremely useful to know, because the `Adldap\Adldap` class is a container that stores each of your LDAP connections. 10 | 11 | For example: 12 | 13 | ```php 14 | use Adldap\Laravel\Facades\Adldap; 15 | 16 | // Returns instance of `Adldap\Adldap` 17 | $adldap = Adldap::getFacadeRoot(); 18 | ``` 19 | 20 | For brevity, please take a look at the core [Adldap2 documentation](https://adldap2.github.io/Adldap2/#/setup?id=getting-started) for usage. 21 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © Steve Bauman 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | :warning: Project No Longer Maintained :warning: 3 |

4 | 5 |

6 | Consider migrating to its direct replacement 7 | LdapRecord-Laravel. 8 |

9 | 10 |

11 | 12 | Read Why 13 | 14 |

15 | 16 |
17 | 18 |

Adldap2 - Laravel

19 | 20 |

21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 |

30 | Easy configuration, access, management and authentication to LDAP servers utilizing the core 31 | Adldap2 repository. 32 |

33 | 34 |

35 | Quickstart 36 | · 37 | Documentation 38 |

39 | 40 | - **Authenticate LDAP users into your application.** Using the built-in authentication driver, easily allow 41 | LDAP users to log into your application and control which users can login via [Scopes](https://adldap2.github.io/Adldap2-Laravel/#/auth/setup?id=scopes) and [Rules](https://adldap2.github.io/Adldap2-Laravel/#/auth/setup?id=rules). 42 | 43 | - **Easily Import & Synchronize LDAP users.** Users can be imported into your database upon first login, 44 | or you can import your entire directory via a simple [command](https://adldap2.github.io/Adldap2-Laravel/#/auth/importing): `php artisan adldap:import`. 45 | 46 | - **Eloquent like Query Builder.** Search for LDAP records with a [fluent and easy to use interface](https://adldap2.github.io/Adldap2/#/searching) you're used to. You'll feel right at home. 47 | 48 | - **Active Record LDAP Models.** LDAP records are returned as [individual models](https://adldap2.github.io/Adldap2/#/models/model). Easily create 49 | and update models then persist them to your LDAP server with a simple `save()`. 50 | -------------------------------------------------------------------------------- /src/AdldapAuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | makeUserProvider($app['hash'], $config); 32 | }); 33 | 34 | if ($this->app->runningInConsole()) { 35 | $config = __DIR__.'/Config/auth.php'; 36 | 37 | $this->publishes([ 38 | $config => config_path('ldap_auth.php'), 39 | ]); 40 | } 41 | 42 | // Register the import command. 43 | $this->commands(Import::class); 44 | } 45 | 46 | /** 47 | * Register the service provider. 48 | * 49 | * @return void 50 | */ 51 | public function register() 52 | { 53 | // Bind the user resolver instance into the IoC. 54 | $this->app->bind(ResolverInterface::class, function () { 55 | return new UserResolver( 56 | $this->app->make(AdldapInterface::class) 57 | ); 58 | }); 59 | 60 | // Here we will register the event listener that will bind the users LDAP 61 | // model to their Eloquent model upon authentication (if configured). 62 | // This allows us to utilize their LDAP model right 63 | // after authentication has passed. 64 | Event::listen([Login::class, Authenticated::class], Listeners\BindsLdapUserModel::class); 65 | 66 | if ($this->isLogging()) { 67 | // If logging is enabled, we will set up our event listeners that 68 | // log each event fired throughout the authentication process. 69 | foreach ($this->getLoggingEvents() as $event => $listener) { 70 | Event::listen($event, $listener); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Returns a new Adldap user provider. 77 | * 78 | * @param Hasher $hasher 79 | * @param array $config 80 | * 81 | * @throws RuntimeException 82 | * 83 | * @return \Illuminate\Contracts\Auth\UserProvider 84 | */ 85 | protected function makeUserProvider(Hasher $hasher, array $config) 86 | { 87 | $provider = Config::get('ldap_auth.provider', DatabaseUserProvider::class); 88 | 89 | // The DatabaseUserProvider requires a model to be configured 90 | // in the configuration. We will validate this here. 91 | if (is_a($provider, DatabaseUserProvider::class, $allowString = true)) { 92 | // We will try to retrieve their model from the config file, 93 | // otherwise we will try to use the providers config array. 94 | $model = Config::get('ldap_auth.model') ?? Arr::get($config, 'model'); 95 | 96 | if (! $model) { 97 | throw new RuntimeException( 98 | "No model is configured. You must configure a model to use with the {$provider}." 99 | ); 100 | } 101 | 102 | return new $provider($hasher, $model); 103 | } 104 | 105 | return new $provider(); 106 | } 107 | 108 | /** 109 | * Determines if authentication requests are logged. 110 | * 111 | * @return bool 112 | */ 113 | protected function isLogging() 114 | { 115 | return Config::get('ldap_auth.logging.enabled', false); 116 | } 117 | 118 | /** 119 | * Returns the configured authentication events to log. 120 | * 121 | * @return array 122 | */ 123 | protected function getLoggingEvents() 124 | { 125 | return Config::get('ldap_auth.logging.events', []); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/AdldapServiceProvider.php: -------------------------------------------------------------------------------- 1 | isLogging()) { 34 | Adldap::setLogger(logger()); 35 | } 36 | 37 | if ($this->isLumen()) { 38 | return; 39 | } 40 | 41 | if ($this->app->runningInConsole()) { 42 | $config = __DIR__.'/Config/config.php'; 43 | 44 | $this->publishes([ 45 | $config => config_path('ldap.php'), 46 | ]); 47 | } 48 | } 49 | 50 | /** 51 | * Register the service provider. 52 | * 53 | * @return void 54 | */ 55 | public function register() 56 | { 57 | // Bind the Adldap contract to the Adldap object 58 | // in the IoC for dependency injection. 59 | $this->app->singleton(AdldapInterface::class, function (Container $app) { 60 | $config = $app->make('config')->get('ldap'); 61 | 62 | // Verify configuration exists. 63 | if (is_null($config)) { 64 | $message = 'Adldap configuration could not be found. Try re-publishing using `php artisan vendor:publish`.'; 65 | 66 | throw new \RuntimeException($message); 67 | } 68 | 69 | return $this->addProviders($this->newAdldap(), $config['connections']); 70 | }); 71 | } 72 | 73 | /** 74 | * Get the services provided by the provider. 75 | * 76 | * @return array 77 | */ 78 | public function provides() 79 | { 80 | return [AdldapInterface::class]; 81 | } 82 | 83 | /** 84 | * Adds providers to the specified Adldap instance. 85 | * 86 | * If a provider is configured to auto connect, 87 | * this method will throw a BindException. 88 | * 89 | * @param Adldap $ldap 90 | * @param array $connections 91 | * 92 | * @return Adldap 93 | */ 94 | protected function addProviders(AdldapInterface $ldap, array $connections = []) 95 | { 96 | // Go through each connection and construct a Provider. 97 | foreach ($connections as $name => $config) { 98 | // Create a new connection with its configured name. 99 | $connection = new $config['connection']($name); 100 | 101 | // Create a new provider. 102 | $provider = $this->newProvider( 103 | $config['settings'], 104 | $connection 105 | ); 106 | 107 | // If auto connect is enabled, an attempt will be made to bind to 108 | // the LDAP server with the configured credentials. If this 109 | // fails then the exception will be logged (if enabled). 110 | if ($this->shouldAutoConnect($config)) { 111 | try { 112 | $provider->connect(); 113 | } catch (AdldapException $e) { 114 | if ($this->isLogging()) { 115 | logger()->error($e); 116 | } 117 | } 118 | } 119 | 120 | // Add the provider to the LDAP container. 121 | $ldap->addProvider($provider, $name); 122 | } 123 | 124 | return $ldap; 125 | } 126 | 127 | /** 128 | * Returns a new Adldap instance. 129 | * 130 | * @return Adldap 131 | */ 132 | protected function newAdldap() 133 | { 134 | return new Adldap(); 135 | } 136 | 137 | /** 138 | * Returns a new LDAP Provider instance. 139 | * 140 | * @param array $configuration 141 | * @param ConnectionInterface|null $connection 142 | * 143 | * @return Provider 144 | */ 145 | protected function newProvider($configuration = [], ConnectionInterface $connection = null) 146 | { 147 | return new Provider($configuration, $connection); 148 | } 149 | 150 | /** 151 | * Determines if the given settings has auto connect enabled. 152 | * 153 | * @param array $settings 154 | * 155 | * @return bool 156 | */ 157 | protected function shouldAutoConnect(array $settings) 158 | { 159 | return array_key_exists('auto_connect', $settings) 160 | && $settings['auto_connect'] === true; 161 | } 162 | 163 | /** 164 | * Determines whether logging is enabled. 165 | * 166 | * @return bool 167 | */ 168 | protected function isLogging() 169 | { 170 | return Config::get('ldap.logging', false); 171 | } 172 | 173 | /** 174 | * Determines if the current application is a Lumen instance. 175 | * 176 | * @return bool 177 | */ 178 | protected function isLumen() 179 | { 180 | return Str::contains($this->app->version(), 'Lumen'); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Auth/DatabaseUserProvider.php: -------------------------------------------------------------------------------- 1 | eloquent = new EloquentUserProvider($hasher, $model); 50 | } 51 | 52 | /** 53 | * Forward missing method calls to the underlying Eloquent provider. 54 | * 55 | * @param string $method 56 | * @param mixed $parameters 57 | * 58 | * @return mixed 59 | */ 60 | public function __call($method, $parameters) 61 | { 62 | return $this->forwardCallTo($this->eloquent, $method, $parameters); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function retrieveById($identifier) 69 | { 70 | return $this->eloquent->retrieveById($identifier); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function retrieveByToken($identifier, $token) 77 | { 78 | return $this->eloquent->retrieveByToken($identifier, $token); 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function updateRememberToken(Authenticatable $user, $token) 85 | { 86 | $this->eloquent->updateRememberToken($user, $token); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function retrieveByCredentials(array $credentials) 93 | { 94 | $user = null; 95 | 96 | try { 97 | $user = Resolver::byCredentials($credentials); 98 | } catch (BindException $e) { 99 | if (! $this->isFallingBack()) { 100 | throw $e; 101 | } 102 | } 103 | 104 | if ($user instanceof User) { 105 | return $this->setAndImportAuthenticatingUser($user); 106 | } 107 | 108 | if ($this->isFallingBack()) { 109 | return $this->eloquent->retrieveByCredentials($credentials); 110 | } 111 | } 112 | 113 | /** 114 | * Set and import the authenticating LDAP user. 115 | * 116 | * @param User $user 117 | * 118 | * @return \Illuminate\Database\Eloquent\Model 119 | */ 120 | protected function setAndImportAuthenticatingUser(User $user) 121 | { 122 | // Set the currently authenticating LDAP user. 123 | $this->user = $user; 124 | 125 | Event::dispatch(new DiscoveredWithCredentials($user)); 126 | 127 | // Import / locate the local user account. 128 | return Bus::dispatch( 129 | new Import($user, $this->eloquent->createModel()) 130 | ); 131 | } 132 | 133 | /** 134 | * {@inheritdoc} 135 | */ 136 | public function validateCredentials(Authenticatable $model, array $credentials) 137 | { 138 | // If the user exists in the local database, fallback is enabled, 139 | // and no LDAP user is was located for authentication, we will 140 | // perform standard eloquent authentication to "fallback" to. 141 | if ( 142 | $model->exists 143 | && $this->isFallingBack() 144 | && ! $this->user instanceof User 145 | ) { 146 | return $this->eloquent->validateCredentials($model, $credentials); 147 | } 148 | 149 | if (! Resolver::authenticate($this->user, $credentials)) { 150 | return false; 151 | } 152 | 153 | Event::dispatch(new AuthenticatedWithCredentials($this->user, $model)); 154 | 155 | // Here we will perform authorization on the LDAP user. If all 156 | // validation rules pass, we will allow the authentication 157 | // attempt. Otherwise, it is automatically rejected. 158 | if (! $this->passesValidation($this->user, $model)) { 159 | Event::dispatch(new AuthenticationRejected($this->user, $model)); 160 | 161 | return false; 162 | } 163 | 164 | Bus::dispatch(new SyncPassword($model, $credentials)); 165 | 166 | $model->save(); 167 | 168 | if ($model->wasRecentlyCreated) { 169 | // If the model was recently created, they 170 | // have been imported successfully. 171 | Event::dispatch(new Imported($this->user, $model)); 172 | } 173 | 174 | Event::dispatch(new AuthenticationSuccessful($this->user, $model)); 175 | 176 | return true; 177 | } 178 | 179 | /** 180 | * Determines if login fallback is enabled. 181 | * 182 | * @return bool 183 | */ 184 | protected function isFallingBack() 185 | { 186 | return Config::get('ldap_auth.login_fallback', false); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/Auth/ForwardsCalls.php: -------------------------------------------------------------------------------- 1 | {$method}(...$parameters); 25 | } catch (Error | BadMethodCallException $e) { 26 | $pattern = '~^Call to undefined method (?P[^:]+)::(?P[^\(]+)\(\)$~'; 27 | 28 | if (! preg_match($pattern, $e->getMessage(), $matches)) { 29 | throw $e; 30 | } 31 | 32 | if ($matches['class'] != get_class($object) || 33 | $matches['method'] != $method) { 34 | throw $e; 35 | } 36 | 37 | static::throwBadMethodCallException($method); 38 | } 39 | } 40 | 41 | /** 42 | * Throw a bad method call exception for the given method. 43 | * 44 | * @param string $method 45 | * 46 | * @throws BadMethodCallException 47 | * 48 | * @return void 49 | */ 50 | protected static function throwBadMethodCallException($method) 51 | { 52 | throw new BadMethodCallException(sprintf( 53 | 'Call to undefined method %s::%s()', static::class, $method 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Auth/NoDatabaseUserProvider.php: -------------------------------------------------------------------------------- 1 | passesValidation($user)) { 65 | Event::dispatch(new AuthenticationSuccessful($user)); 66 | 67 | return true; 68 | } 69 | 70 | Event::dispatch(new AuthenticationRejected($user)); 71 | } 72 | 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Auth/UserProvider.php: -------------------------------------------------------------------------------- 1 | getUsers(); 57 | 58 | $count = count($users); 59 | 60 | if ($count === 0) { 61 | return $this->info('There were no users found to import.'); 62 | } elseif ($count === 1) { 63 | $this->info("Found user '{$users[0]->getCommonName()}'."); 64 | } else { 65 | $this->info("Found {$count} user(s)."); 66 | } 67 | 68 | if ( 69 | $this->input->isInteractive() && 70 | $this->confirm('Would you like to display the user(s) to be imported / synchronized?', $default = false) 71 | ) { 72 | $this->display($users); 73 | } 74 | 75 | if ( 76 | ! $this->input->isInteractive() || 77 | $this->confirm('Would you like these users to be imported / synchronized?', $default = true) 78 | ) { 79 | $imported = $this->import($users); 80 | 81 | $this->info("Successfully imported / synchronized {$imported} user(s)."); 82 | } else { 83 | $this->info('Okay, no users were imported / synchronized.'); 84 | } 85 | } 86 | 87 | /** 88 | * Imports the specified users and returns the total 89 | * number of users successfully imported. 90 | * 91 | * @param array $users 92 | * 93 | * @return int 94 | */ 95 | public function import(array $users = []): int 96 | { 97 | $imported = 0; 98 | 99 | $this->output->progressStart(count($users)); 100 | 101 | foreach ($users as $user) { 102 | try { 103 | // Import the user and retrieve it's model. 104 | $model = Bus::dispatch( 105 | new ImportUser($user, $this->model()) 106 | ); 107 | 108 | // Set the users password. 109 | Bus::dispatch(new SyncPassword($model)); 110 | 111 | // Save the returned model. 112 | $this->save($user, $model); 113 | 114 | if ($this->isDeleting()) { 115 | $this->delete($user, $model); 116 | } 117 | 118 | if ($this->isRestoring()) { 119 | $this->restore($user, $model); 120 | } 121 | 122 | $imported++; 123 | } catch (Exception $e) { 124 | // Log the unsuccessful import. 125 | if ($this->isLogging()) { 126 | logger()->error("Unable to import user {$user->getCommonName()}. {$e->getMessage()}"); 127 | } 128 | } 129 | 130 | $this->output->progressAdvance(); 131 | } 132 | 133 | $this->output->progressFinish(); 134 | 135 | return $imported; 136 | } 137 | 138 | /** 139 | * Displays the given users in a table. 140 | * 141 | * @param array $users 142 | * 143 | * @return void 144 | */ 145 | public function display(array $users = []) 146 | { 147 | $headers = ['Name', 'Account Name', 'UPN']; 148 | 149 | $data = []; 150 | 151 | array_map(function (User $user) use (&$data) { 152 | $data[] = [ 153 | 'name' => $user->getCommonName(), 154 | 'account_name' => $user->getAccountName(), 155 | 'upn' => $user->getUserPrincipalName(), 156 | ]; 157 | }, $users); 158 | 159 | $this->table($headers, $data); 160 | } 161 | 162 | /** 163 | * Returns true / false if the current import is being logged. 164 | * 165 | * @return bool 166 | */ 167 | public function isLogging(): bool 168 | { 169 | return ! $this->option('no-log'); 170 | } 171 | 172 | /** 173 | * Returns true / false if users are being deleted 174 | * if their account is disabled in LDAP. 175 | * 176 | * @return bool 177 | */ 178 | public function isDeleting(): bool 179 | { 180 | return $this->option('delete') == 'true'; 181 | } 182 | 183 | /** 184 | * Returns true / false if users are being restored 185 | * if their account is enabled in LDAP. 186 | * 187 | * @return bool 188 | */ 189 | public function isRestoring(): bool 190 | { 191 | return $this->option('restore') == 'true'; 192 | } 193 | 194 | /** 195 | * Retrieves users to be imported. 196 | * 197 | * @throws \Adldap\Models\ModelNotFoundException 198 | * 199 | * @return array 200 | */ 201 | public function getUsers(): array 202 | { 203 | /** @var \Adldap\Query\Builder $query */ 204 | $query = Resolver::query(); 205 | 206 | if ($filter = $this->option('filter')) { 207 | // If the filter option was given, we'll 208 | // insert it into our search query. 209 | $query->rawFilter($filter); 210 | } 211 | 212 | if ($user = $this->argument('user')) { 213 | $users = [$query->findOrFail($user)]; 214 | } else { 215 | // Retrieve all users. We'll paginate our search in case we 216 | // hit the 1000 record hard limit of active directory. 217 | $users = $query->paginate()->getResults(); 218 | } 219 | 220 | // We need to filter our results to make sure they are 221 | // only users. In some cases, Contact models may be 222 | // returned due the possibility of them 223 | // existing in the same scope. 224 | return array_filter($users, function ($user) { 225 | return $user instanceof User; 226 | }); 227 | } 228 | 229 | /** 230 | * Saves the specified user with its model. 231 | * 232 | * @param User $user 233 | * @param Model $model 234 | * 235 | * @return bool 236 | */ 237 | protected function save(User $user, Model $model): bool 238 | { 239 | if ($model->save() && $model->wasRecentlyCreated) { 240 | Event::dispatch(new Imported($user, $model)); 241 | 242 | // Log the successful import. 243 | if ($this->isLogging()) { 244 | logger()->info("Imported user {$user->getCommonName()}"); 245 | } 246 | 247 | return true; 248 | } 249 | 250 | return false; 251 | } 252 | 253 | /** 254 | * Restores soft-deleted models if their LDAP account is enabled. 255 | * 256 | * @param User $user 257 | * @param Model $model 258 | * 259 | * @return void 260 | */ 261 | protected function restore(User $user, Model $model) 262 | { 263 | if ( 264 | $this->isUsingSoftDeletes($model) && 265 | $model->trashed() && 266 | $user->isEnabled() 267 | ) { 268 | // If the model has soft-deletes enabled, the model is 269 | // currently deleted, and the LDAP user account 270 | // is enabled, we'll restore the users model. 271 | $model->restore(); 272 | 273 | if ($this->isLogging()) { 274 | $type = get_class($user->getSchema()); 275 | 276 | logger()->info("Restored user {$user->getCommonName()}. Their {$type} user account has been re-enabled."); 277 | } 278 | } 279 | } 280 | 281 | /** 282 | * Soft deletes the specified model if their LDAP account is disabled. 283 | * 284 | * @param User $user 285 | * @param Model $model 286 | * 287 | * @throws Exception 288 | * 289 | * @return void 290 | */ 291 | protected function delete(User $user, Model $model) 292 | { 293 | if ( 294 | $this->isUsingSoftDeletes($model) && 295 | ! $model->trashed() && 296 | $user->isDisabled() 297 | ) { 298 | // If deleting is enabled, the model supports soft deletes, the model 299 | // isn't already deleted, and the LDAP user is disabled, we'll 300 | // go ahead and delete the users model. 301 | $model->delete(); 302 | 303 | if ($this->isLogging()) { 304 | $type = get_class($user->getSchema()); 305 | 306 | logger()->info("Soft-deleted user {$user->getCommonName()}. Their {$type} user account is disabled."); 307 | } 308 | } 309 | } 310 | 311 | /** 312 | * Set and create a new instance of the eloquent model to use. 313 | * 314 | * @return Model 315 | */ 316 | protected function model(): Model 317 | { 318 | if (! $this->model) { 319 | $this->model = $this->option('model') ?? Config::get('ldap_auth.model') ?: $this->determineModel(); 320 | } 321 | 322 | return new $this->model(); 323 | } 324 | 325 | /** 326 | * Retrieves the model by checking the configured LDAP authentication providers. 327 | * 328 | * @throws UnexpectedValueException 329 | * 330 | * @return string 331 | */ 332 | protected function determineModel() 333 | { 334 | // Retrieve all of the configured authentication providers that 335 | // use the LDAP driver and have a configured model. 336 | $providers = Arr::where(Config::get('auth.providers'), function ($value, $key) { 337 | return $value['driver'] == 'ldap' && array_key_exists('model', $value); 338 | }); 339 | 340 | // Pull the first driver and return a new model instance. 341 | if ($ldap = reset($providers)) { 342 | return $ldap['model']; 343 | } 344 | 345 | throw new UnexpectedValueException( 346 | 'Unable to retrieve LDAP authentication driver. Did you forget to configure it?' 347 | ); 348 | } 349 | 350 | /** 351 | * Returns true / false if the model is using soft deletes 352 | * by checking if the model contains a `trashed` method. 353 | * 354 | * @param Model $model 355 | * 356 | * @return bool 357 | */ 358 | protected function isUsingSoftDeletes(Model $model): bool 359 | { 360 | return method_exists($model, 'trashed'); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/Commands/Import.php: -------------------------------------------------------------------------------- 1 | user = $user; 57 | $this->model = $model; 58 | } 59 | 60 | /** 61 | * Imports the current LDAP user. 62 | * 63 | * @return Model 64 | */ 65 | public function handle() 66 | { 67 | // Here we'll try to locate our local user model from 68 | // the LDAP users model. If one isn't located, 69 | // we'll create a new one for them. 70 | $model = $this->findUser() ?: $this->model->newInstance(); 71 | 72 | if (! $model->exists) { 73 | Event::dispatch(new Importing($this->user, $model)); 74 | } 75 | 76 | Event::dispatch(new Synchronizing($this->user, $model)); 77 | 78 | $this->sync($model); 79 | 80 | Event::dispatch(new Synchronized($this->user, $model)); 81 | 82 | return $model; 83 | } 84 | 85 | /** 86 | * Retrieves an eloquent user by their GUID or their username. 87 | * 88 | * @throws UnexpectedValueException 89 | * 90 | * @return Model|null 91 | */ 92 | protected function findUser() 93 | { 94 | $query = $this->model->newQuery(); 95 | 96 | if ($query->getMacro('withTrashed')) { 97 | // If the withTrashed macro exists on our User model, then we must be 98 | // using soft deletes. We need to make sure we include these 99 | // results so we don't create duplicate user records. 100 | $query->withTrashed(); 101 | } 102 | 103 | /** @var \Illuminate\Database\Eloquent\Scope $scope */ 104 | $scope = new static::$scope( 105 | $this->getUserObjectGuid(), 106 | $this->getUserUsername() 107 | ); 108 | 109 | $scope->apply($query, $this->model); 110 | 111 | return $query->first(); 112 | } 113 | 114 | /** 115 | * Fills a models attributes by the specified Users attributes. 116 | * 117 | * @param Model $model 118 | * 119 | * @return void 120 | */ 121 | protected function sync(Model $model) 122 | { 123 | // Set the users LDAP identifier. 124 | $model->setAttribute( 125 | Resolver::getDatabaseIdColumn(), $this->getUserObjectGuid() 126 | ); 127 | 128 | foreach ($this->getLdapSyncAttributes() as $modelField => $ldapField) { 129 | // If the field is a loaded class and contains a `handle()` method, 130 | // we need to construct the attribute handler. 131 | if ($this->isHandler($ldapField)) { 132 | // We will construct the attribute handler using Laravel's 133 | // IoC to allow developers to utilize application 134 | // dependencies in the constructor. 135 | /** @var mixed $handler */ 136 | $handler = app($ldapField); 137 | 138 | $handler->handle($this->user, $model); 139 | } else { 140 | // We'll try to retrieve the value from the LDAP model. If the LDAP field is a string, 141 | // we'll assume the developer wants the attribute, or a null value. Otherwise, 142 | // the raw value of the LDAP field will be used. 143 | $model->{$modelField} = is_string($ldapField) ? $this->user->getFirstAttribute($ldapField) : $ldapField; 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Returns the LDAP users configured username. 150 | * 151 | * @throws UnexpectedValueException 152 | * 153 | * @return string 154 | */ 155 | protected function getUserUsername() 156 | { 157 | $attribute = Resolver::getLdapDiscoveryAttribute(); 158 | 159 | $username = $this->user->getFirstAttribute($attribute); 160 | 161 | if (trim($username) == false) { 162 | throw new UnexpectedValueException( 163 | "Unable to locate a user without a {$attribute}" 164 | ); 165 | } 166 | 167 | return $username; 168 | } 169 | 170 | /** 171 | * Returns the LDAP users object GUID. 172 | * 173 | * @throws UnexpectedValueException 174 | * 175 | * @return string 176 | */ 177 | protected function getUserObjectGuid() 178 | { 179 | $guid = $this->user->getConvertedGuid(); 180 | 181 | if (trim($guid) == false) { 182 | $attribute = $this->user->getSchema()->objectGuid(); 183 | 184 | throw new UnexpectedValueException( 185 | "Unable to locate a user without a {$attribute}" 186 | ); 187 | } 188 | 189 | return $guid; 190 | } 191 | 192 | /** 193 | * Determines if the given handler value is a class that contains the 'handle' method. 194 | * 195 | * @param mixed $handler 196 | * 197 | * @return bool 198 | */ 199 | protected function isHandler($handler) 200 | { 201 | return is_string($handler) && class_exists($handler) && method_exists($handler, 'handle'); 202 | } 203 | 204 | /** 205 | * Returns the configured LDAP sync attributes. 206 | * 207 | * @return array 208 | */ 209 | protected function getLdapSyncAttributes() 210 | { 211 | return Config::get('ldap_auth.sync_attributes', [ 212 | 'email' => 'userprincipalname', 213 | 'name' => 'cn', 214 | ]); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Commands/SyncPassword.php: -------------------------------------------------------------------------------- 1 | model = $model; 36 | $this->credentials = $credentials; 37 | } 38 | 39 | /** 40 | * Sets the password on the users model. 41 | * 42 | * @return Model 43 | */ 44 | public function handle(): Model 45 | { 46 | if ($this->hasPasswordColumn()) { 47 | $password = $this->canSync() ? 48 | $this->password() : Str::random(); 49 | 50 | if ($this->passwordNeedsUpdate($password)) { 51 | $this->applyPassword($password); 52 | } 53 | } 54 | 55 | return $this->model; 56 | } 57 | 58 | /** 59 | * Applies the password to the users model. 60 | * 61 | * @param string $password 62 | * 63 | * @return void 64 | */ 65 | protected function applyPassword($password) 66 | { 67 | // If the model has a mutator for the password field, we 68 | // can assume hashing passwords is taken care of. 69 | // Otherwise, we will hash it normally. 70 | $password = $this->model->hasSetMutator($this->column()) ? $password : Hash::make($password); 71 | 72 | $this->model->setAttribute($this->column(), $password); 73 | } 74 | 75 | /** 76 | * Determines if the current model requires a password update. 77 | * 78 | * This checks if the model does not currently have a 79 | * password, or if the password fails a hash check. 80 | * 81 | * @param string|null $password 82 | * 83 | * @return bool 84 | */ 85 | protected function passwordNeedsUpdate($password = null): bool 86 | { 87 | $current = $this->currentModelPassword(); 88 | 89 | if ($current !== null && $this->canSync()) { 90 | return ! Hash::check($password, $current); 91 | } 92 | 93 | return is_null($current); 94 | } 95 | 96 | /** 97 | * Determines if the developer has configured a password column. 98 | * 99 | * @return bool 100 | */ 101 | protected function hasPasswordColumn(): bool 102 | { 103 | return ! is_null($this->column()); 104 | } 105 | 106 | /** 107 | * Retrieves the password from the users credentials. 108 | * 109 | * @return string|null 110 | */ 111 | protected function password() 112 | { 113 | return Arr::get($this->credentials, 'password'); 114 | } 115 | 116 | /** 117 | * Retrieves the current models hashed password. 118 | * 119 | * @return string|null 120 | */ 121 | protected function currentModelPassword() 122 | { 123 | return $this->model->getAttribute($this->column()); 124 | } 125 | 126 | /** 127 | * Determines if we're able to sync the models password with the current credentials. 128 | * 129 | * @return bool 130 | */ 131 | protected function canSync(): bool 132 | { 133 | return array_key_exists('password', $this->credentials) && $this->syncing(); 134 | } 135 | 136 | /** 137 | * Determines if the password should be synchronized. 138 | * 139 | * @return bool 140 | */ 141 | protected function syncing(): bool 142 | { 143 | return Config::get('ldap_auth.passwords.sync', false); 144 | } 145 | 146 | /** 147 | * Retrieves the password column to use. 148 | * 149 | * @return string|null 150 | */ 151 | protected function column() 152 | { 153 | return Config::get('ldap_auth.passwords.column', 'password'); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Commands/UserImportScope.php: -------------------------------------------------------------------------------- 1 | guid = $guid; 35 | $this->username = $username; 36 | } 37 | 38 | /** 39 | * Apply the scope to a given Eloquent query builder. 40 | * 41 | * @param Builder $query 42 | * @param Model $model 43 | * 44 | * @return void 45 | */ 46 | public function apply(Builder $query, Model $model) 47 | { 48 | $this->user($query); 49 | } 50 | 51 | /** 52 | * Applies the user scope to the given Eloquent query builder. 53 | * 54 | * @param Builder $query 55 | */ 56 | protected function user(Builder $query) 57 | { 58 | // We'll try to locate the user by their object guid, 59 | // otherwise we'll locate them by their username. 60 | $query 61 | ->where(Resolver::getDatabaseIdColumn(), '=', $this->getGuid()) 62 | ->orWhere(Resolver::getDatabaseUsernameColumn(), '=', $this->getUsername()); 63 | } 64 | 65 | /** 66 | * Returns the LDAP users object guid. 67 | * 68 | * @return string 69 | */ 70 | protected function getGuid() 71 | { 72 | return $this->guid; 73 | } 74 | 75 | /** 76 | * Returns the LDAP users username. 77 | * 78 | * @return string 79 | */ 80 | protected function getUsername() 81 | { 82 | return $this->username; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Config/auth.php: -------------------------------------------------------------------------------- 1 | env('LDAP_CONNECTION', 'default'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Provider 21 | |-------------------------------------------------------------------------- 22 | | 23 | | The LDAP authentication provider to use depending 24 | | if you require database synchronization. 25 | | 26 | | For synchronizing LDAP users to your local applications database, use the provider: 27 | | 28 | | Adldap\Laravel\Auth\DatabaseUserProvider::class 29 | | 30 | | Otherwise, if you just require LDAP authentication, use the provider: 31 | | 32 | | Adldap\Laravel\Auth\NoDatabaseUserProvider::class 33 | | 34 | */ 35 | 36 | 'provider' => Adldap\Laravel\Auth\DatabaseUserProvider::class, 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Model 41 | |-------------------------------------------------------------------------- 42 | | 43 | | The model to utilize for authentication and importing. 44 | | 45 | | This option is only applicable to the DatabaseUserProvider. 46 | | 47 | */ 48 | 49 | 'model' => App\User::class, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Rules 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Rules allow you to control user authentication requests depending on scenarios. 57 | | 58 | | You can create your own rules and insert them here. 59 | | 60 | | All rules must extend from the following class: 61 | | 62 | | Adldap\Laravel\Validation\Rules\Rule 63 | | 64 | */ 65 | 66 | 'rules' => [ 67 | 68 | // Denys deleted users from authenticating. 69 | 70 | Adldap\Laravel\Validation\Rules\DenyTrashed::class, 71 | 72 | // Allows only manually imported users to authenticate. 73 | 74 | // Adldap\Laravel\Validation\Rules\OnlyImported::class, 75 | 76 | ], 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Scopes 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Scopes allow you to restrict the LDAP query that locates 84 | | users upon import and authentication. 85 | | 86 | | All scopes must implement the following interface: 87 | | 88 | | Adldap\Laravel\Scopes\ScopeInterface 89 | | 90 | */ 91 | 92 | 'scopes' => [ 93 | 94 | // Only allows users with a user principal name to authenticate. 95 | // Suitable when using ActiveDirectory. 96 | // Adldap\Laravel\Scopes\UpnScope::class, 97 | 98 | // Only allows users with a uid to authenticate. 99 | // Suitable when using OpenLDAP. 100 | // Adldap\Laravel\Scopes\UidScope::class, 101 | 102 | ], 103 | 104 | 'identifiers' => [ 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | LDAP 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Locate Users By: 112 | | 113 | | This value is the users attribute you would like to locate LDAP 114 | | users by in your directory. 115 | | 116 | | For example, using the default configuration below, if you're 117 | | authenticating users with an email address, your LDAP server 118 | | will be queried for a user with the a `userprincipalname` 119 | | equal to the entered email address. 120 | | 121 | | Bind Users By: 122 | | 123 | | This value is the users attribute you would 124 | | like to use to bind to your LDAP server. 125 | | 126 | | For example, when a user is located by the above attribute, 127 | | the users attribute you specify below will be used as 128 | | the 'username' to bind to your LDAP server. 129 | | 130 | | This is usually their distinguished name. 131 | | 132 | */ 133 | 134 | 'ldap' => [ 135 | 136 | 'locate_users_by' => 'userprincipalname', 137 | 138 | 'bind_users_by' => 'distinguishedname', 139 | 140 | ], 141 | 142 | 'database' => [ 143 | 144 | /* 145 | |-------------------------------------------------------------------------- 146 | | GUID Column 147 | |-------------------------------------------------------------------------- 148 | | 149 | | The value of this option is the database column that will contain the 150 | | LDAP users global identifier. This column does not need to be added 151 | | to the sync attributes below. It is synchronized automatically. 152 | | 153 | | This option is only applicable to the DatabaseUserProvider. 154 | | 155 | */ 156 | 157 | 'guid_column' => 'objectguid', 158 | 159 | /* 160 | |-------------------------------------------------------------------------- 161 | | Username Column 162 | |-------------------------------------------------------------------------- 163 | | 164 | | The value of this option is the database column that contains your 165 | | users login username. 166 | | 167 | | This column must be added to your sync attributes below to be 168 | | properly synchronized. 169 | | 170 | | This option is only applicable to the DatabaseUserProvider. 171 | | 172 | */ 173 | 174 | 'username_column' => 'email', 175 | 176 | ], 177 | 178 | /* 179 | |-------------------------------------------------------------------------- 180 | | Windows Authentication Middleware (SSO) 181 | |-------------------------------------------------------------------------- 182 | | 183 | | Local Users By: 184 | | 185 | | This value is the users attribute you would like to locate LDAP 186 | | users by in your directory. 187 | | 188 | | For example, if 'samaccountname' is the value, then your LDAP server is 189 | | queried for a user with the 'samaccountname' equal to the value of 190 | | $_SERVER['AUTH_USER']. 191 | | 192 | | If a user is found, they are imported (if using the DatabaseUserProvider) 193 | | into your local database, then logged in. 194 | | 195 | | Server Key: 196 | | 197 | | This value represents the 'key' of the $_SERVER 198 | | array to pull the users account name from. 199 | | 200 | | For example, $_SERVER['AUTH_USER']. 201 | | 202 | */ 203 | 204 | 'windows' => [ 205 | 206 | 'locate_users_by' => 'samaccountname', 207 | 208 | 'server_key' => 'AUTH_USER', 209 | 210 | ], 211 | 212 | ], 213 | 214 | 'passwords' => [ 215 | 216 | /* 217 | |-------------------------------------------------------------------------- 218 | | Password Sync 219 | |-------------------------------------------------------------------------- 220 | | 221 | | The password sync option allows you to automatically synchronize users 222 | | LDAP passwords to your local database. These passwords are hashed 223 | | natively by Laravel using the Hash::make() method. 224 | | 225 | | Enabling this option would also allow users to login to their accounts 226 | | using the password last used when an LDAP connection was present. 227 | | 228 | | If this option is disabled, the local database account is applied a 229 | | random 16 character hashed password upon first login, and will 230 | | lose access to this account upon loss of LDAP connectivity. 231 | | 232 | | This option is only applicable to the DatabaseUserProvider. 233 | | 234 | */ 235 | 236 | 'sync' => env('LDAP_PASSWORD_SYNC', false), 237 | 238 | /* 239 | |-------------------------------------------------------------------------- 240 | | Column 241 | |-------------------------------------------------------------------------- 242 | | 243 | | This is the column of your users database table 244 | | that is used to store passwords. 245 | | 246 | | Set this to `null` if you do not have a password column. 247 | | 248 | | This option is only applicable to the DatabaseUserProvider. 249 | | 250 | */ 251 | 252 | 'column' => 'password', 253 | 254 | ], 255 | 256 | /* 257 | |-------------------------------------------------------------------------- 258 | | Login Fallback 259 | |-------------------------------------------------------------------------- 260 | | 261 | | The login fallback option allows you to login as a user located in the 262 | | local database if active directory authentication fails. 263 | | 264 | | Set this to true if you would like to enable it. 265 | | 266 | | This option is only applicable to the DatabaseUserProvider. 267 | | 268 | */ 269 | 270 | 'login_fallback' => env('LDAP_LOGIN_FALLBACK', false), 271 | 272 | /* 273 | |-------------------------------------------------------------------------- 274 | | Sync Attributes 275 | |-------------------------------------------------------------------------- 276 | | 277 | | Attributes specified here will be added / replaced on the user model 278 | | upon login, automatically synchronizing and keeping the attributes 279 | | up to date. 280 | | 281 | | The array key represents the users Laravel model key, and 282 | | the value represents the users LDAP attribute. 283 | | 284 | | You **must** include the users login attribute here. 285 | | 286 | | This option is only applicable to the DatabaseUserProvider. 287 | | 288 | */ 289 | 290 | 'sync_attributes' => [ 291 | 292 | 'email' => 'userprincipalname', 293 | 294 | 'name' => 'cn', 295 | 296 | ], 297 | 298 | /* 299 | |-------------------------------------------------------------------------- 300 | | Logging 301 | |-------------------------------------------------------------------------- 302 | | 303 | | User authentication attempts will be logged using Laravel's 304 | | default logger if this setting is enabled. 305 | | 306 | | No credentials are logged, only usernames. 307 | | 308 | | This is usually stored in the '/storage/logs' directory 309 | | in the root of your application. 310 | | 311 | | This option is useful for debugging as well as auditing. 312 | | 313 | | You can freely remove any events you would not like to log below, 314 | | as well as use your own listeners if you would prefer. 315 | | 316 | */ 317 | 318 | 'logging' => [ 319 | 320 | 'enabled' => env('LDAP_LOGGING', true), 321 | 322 | 'events' => [ 323 | 324 | \Adldap\Laravel\Events\Importing::class => \Adldap\Laravel\Listeners\LogImport::class, 325 | \Adldap\Laravel\Events\Synchronized::class => \Adldap\Laravel\Listeners\LogSynchronized::class, 326 | \Adldap\Laravel\Events\Synchronizing::class => \Adldap\Laravel\Listeners\LogSynchronizing::class, 327 | \Adldap\Laravel\Events\Authenticated::class => \Adldap\Laravel\Listeners\LogAuthenticated::class, 328 | \Adldap\Laravel\Events\Authenticating::class => \Adldap\Laravel\Listeners\LogAuthentication::class, 329 | \Adldap\Laravel\Events\AuthenticationFailed::class => \Adldap\Laravel\Listeners\LogAuthenticationFailure::class, 330 | \Adldap\Laravel\Events\AuthenticationRejected::class => \Adldap\Laravel\Listeners\LogAuthenticationRejection::class, 331 | \Adldap\Laravel\Events\AuthenticationSuccessful::class => \Adldap\Laravel\Listeners\LogAuthenticationSuccess::class, 332 | \Adldap\Laravel\Events\DiscoveredWithCredentials::class => \Adldap\Laravel\Listeners\LogDiscovery::class, 333 | \Adldap\Laravel\Events\AuthenticatedWithWindows::class => \Adldap\Laravel\Listeners\LogWindowsAuth::class, 334 | \Adldap\Laravel\Events\AuthenticatedModelTrashed::class => \Adldap\Laravel\Listeners\LogTrashedModel::class, 335 | 336 | ], 337 | ], 338 | 339 | ]; 340 | -------------------------------------------------------------------------------- /src/Config/config.php: -------------------------------------------------------------------------------- 1 | env('LDAP_LOGGING', false), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Connections 24 | |-------------------------------------------------------------------------- 25 | | 26 | | This array stores the connections that are added to Adldap. You can add 27 | | as many connections as you like. 28 | | 29 | | The key is the name of the connection you wish to use and the value is 30 | | an array of configuration settings. 31 | | 32 | */ 33 | 34 | 'connections' => [ 35 | 36 | 'default' => [ 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Auto Connect 41 | |-------------------------------------------------------------------------- 42 | | 43 | | If auto connect is true, Adldap will try to automatically connect to 44 | | your LDAP server in your configuration. This allows you to assume 45 | | connectivity rather than having to connect manually 46 | | in your application. 47 | | 48 | | If this is set to false, you **must** connect manually before running 49 | | LDAP operations. Otherwise, you will receive exceptions. 50 | | 51 | */ 52 | 53 | 'auto_connect' => env('LDAP_AUTO_CONNECT', true), 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Connection 58 | |-------------------------------------------------------------------------- 59 | | 60 | | The connection class to use to run raw LDAP operations on. 61 | | 62 | | Custom connection classes must implement: 63 | | 64 | | Adldap\Connections\ConnectionInterface 65 | | 66 | */ 67 | 68 | 'connection' => Adldap\Connections\Ldap::class, 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Connection Settings 73 | |-------------------------------------------------------------------------- 74 | | 75 | | This connection settings array is directly passed into the Adldap constructor. 76 | | 77 | | Feel free to add or remove settings you don't need. 78 | | 79 | */ 80 | 81 | 'settings' => [ 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Schema 86 | |-------------------------------------------------------------------------- 87 | | 88 | | The schema class to use for retrieving attributes and generating models. 89 | | 90 | | You can also set this option to `null` to use the default schema class. 91 | | 92 | | For OpenLDAP, you must use the schema: 93 | | 94 | | Adldap\Schemas\OpenLDAP::class 95 | | 96 | | For FreeIPA, you must use the schema: 97 | | 98 | | Adldap\Schemas\FreeIPA::class 99 | | 100 | | Custom schema classes must implement Adldap\Schemas\SchemaInterface 101 | | 102 | */ 103 | 104 | 'schema' => Adldap\Schemas\ActiveDirectory::class, 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Account Prefix 109 | |-------------------------------------------------------------------------- 110 | | 111 | | The account prefix option is the prefix of your user accounts in LDAP directory. 112 | | 113 | | This string is prepended to all authenticating users usernames. 114 | | 115 | */ 116 | 117 | 'account_prefix' => env('LDAP_ACCOUNT_PREFIX', ''), 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Account Suffix 122 | |-------------------------------------------------------------------------- 123 | | 124 | | The account suffix option is the suffix of your user accounts in your LDAP directory. 125 | | 126 | | This string is appended to all authenticating users usernames. 127 | | 128 | */ 129 | 130 | 'account_suffix' => env('LDAP_ACCOUNT_SUFFIX', ''), 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Domain Controllers 135 | |-------------------------------------------------------------------------- 136 | | 137 | | The domain controllers option is an array of servers located on your 138 | | network that serve Active Directory. You can insert as many servers or 139 | | as little as you'd like depending on your forest (with the 140 | | minimum of one of course). 141 | | 142 | | These can be IP addresses of your server(s), or the host name. 143 | | 144 | */ 145 | 146 | 'hosts' => explode(' ', env('LDAP_HOSTS', 'corp-dc1.corp.acme.org corp-dc2.corp.acme.org')), 147 | 148 | /* 149 | |-------------------------------------------------------------------------- 150 | | Port 151 | |-------------------------------------------------------------------------- 152 | | 153 | | The port option is used for authenticating and binding to your LDAP server. 154 | | 155 | */ 156 | 157 | 'port' => env('LDAP_PORT', 389), 158 | 159 | /* 160 | |-------------------------------------------------------------------------- 161 | | Timeout 162 | |-------------------------------------------------------------------------- 163 | | 164 | | The timeout option allows you to configure the amount of time in 165 | | seconds that your application waits until a response 166 | | is received from your LDAP server. 167 | | 168 | */ 169 | 170 | 'timeout' => env('LDAP_TIMEOUT', 5), 171 | 172 | /* 173 | |-------------------------------------------------------------------------- 174 | | Base Distinguished Name 175 | |-------------------------------------------------------------------------- 176 | | 177 | | The base distinguished name is the base distinguished name you'd 178 | | like to perform query operations on. An example base DN would be: 179 | | 180 | | dc=corp,dc=acme,dc=org 181 | | 182 | | A correct base DN is required for any query results to be returned. 183 | | 184 | */ 185 | 186 | 'base_dn' => env('LDAP_BASE_DN', 'dc=corp,dc=acme,dc=org'), 187 | 188 | /* 189 | |-------------------------------------------------------------------------- 190 | | LDAP Username & Password 191 | |-------------------------------------------------------------------------- 192 | | 193 | | When connecting to your LDAP server, a username and password is required 194 | | to be able to query and run operations on your server(s). You can 195 | | use any user account that has these permissions. This account 196 | | does not need to be a domain administrator unless you 197 | | require changing and resetting user passwords. 198 | | 199 | */ 200 | 201 | 'username' => env('LDAP_USERNAME', 'username'), 202 | 'password' => env('LDAP_PASSWORD', 'secret'), 203 | 204 | /* 205 | |-------------------------------------------------------------------------- 206 | | Follow Referrals 207 | |-------------------------------------------------------------------------- 208 | | 209 | | The follow referrals option is a boolean to tell active directory 210 | | to follow a referral to another server on your network if the 211 | | server queried knows the information your asking for exists, 212 | | but does not yet contain a copy of it locally. 213 | | 214 | | This option is defaulted to false. 215 | | 216 | */ 217 | 218 | 'follow_referrals' => false, 219 | 220 | /* 221 | |-------------------------------------------------------------------------- 222 | | SSL & TLS 223 | |-------------------------------------------------------------------------- 224 | | 225 | | If you need to be able to change user passwords on your server, then an 226 | | SSL or TLS connection is required. All other operations are allowed 227 | | on unsecured protocols. 228 | | 229 | | One of these options are definitely recommended if you 230 | | have the ability to connect to your server securely. 231 | | 232 | */ 233 | 234 | 'use_ssl' => env('LDAP_USE_SSL', false), 235 | 'use_tls' => env('LDAP_USE_TLS', false), 236 | 237 | ], 238 | 239 | ], 240 | 241 | ], 242 | 243 | ]; 244 | -------------------------------------------------------------------------------- /src/Events/Authenticated.php: -------------------------------------------------------------------------------- 1 | user = $user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Events/AuthenticatedModelTrashed.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/AuthenticatedWithCredentials.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/AuthenticatedWithWindows.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/Authenticating.php: -------------------------------------------------------------------------------- 1 | user = $user; 32 | $this->username = $username; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Events/AuthenticationFailed.php: -------------------------------------------------------------------------------- 1 | user = $user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Events/AuthenticationRejected.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/AuthenticationSuccessful.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/DiscoveredWithCredentials.php: -------------------------------------------------------------------------------- 1 | user = $user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Events/Imported.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/Importing.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/Synchronized.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/Synchronizing.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Facades/Adldap.php: -------------------------------------------------------------------------------- 1 | guard; 27 | } 28 | 29 | // Before we bind the users LDAP model, we will verify they are using 30 | // the Adldap authentication provider, and the required trait. 31 | if ($this->isUsingAdldapProvider($guard) && $this->canBind($event->user)) { 32 | $event->user->setLdapUser( 33 | Resolver::byModel($event->user) 34 | ); 35 | } 36 | } 37 | 38 | /** 39 | * Determines if the Auth Provider is an instance of the Adldap Provider. 40 | * 41 | * @param string|null $guard 42 | * 43 | * @return bool 44 | */ 45 | protected function isUsingAdldapProvider($guard = null): bool 46 | { 47 | return Auth::guard($guard)->getProvider() instanceof DatabaseUserProvider; 48 | } 49 | 50 | /** 51 | * Determines if we're able to bind to the user. 52 | * 53 | * @param Authenticatable $user 54 | * 55 | * @return bool 56 | */ 57 | protected function canBind(Authenticatable $user): bool 58 | { 59 | return array_key_exists(HasLdapUser::class, class_uses_recursive($user)) && is_null($user->ldap); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Listeners/LogAuthenticated.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' has successfully passed LDAP authentication."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogAuthentication.php: -------------------------------------------------------------------------------- 1 | getPrefix().$event->username.$this->getSuffix(); 21 | 22 | Log::info("User '{$event->user->getCommonName()}' is authenticating with username: '{$username}'"); 23 | } 24 | 25 | /** 26 | * Returns the account prefix that is applied to username's. 27 | * 28 | * @return string|null 29 | */ 30 | protected function getPrefix() 31 | { 32 | return Config::get("{$this->getConfigSettingsPath()}.account_prefix"); 33 | } 34 | 35 | /** 36 | * Returns the account suffix that is applied to username's. 37 | * 38 | * @return string|null 39 | */ 40 | protected function getSuffix() 41 | { 42 | return Config::get("{$this->getConfigSettingsPath()}.account_suffix"); 43 | } 44 | 45 | /** 46 | * Returns the current connections configuration path. 47 | * 48 | * @return string 49 | */ 50 | protected function getConfigSettingsPath() 51 | { 52 | $connection = Config::get('ldap_auth.connection'); 53 | 54 | return "ldap.connections.$connection.settings"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Listeners/LogAuthenticationFailure.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' has failed LDAP authentication."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogAuthenticationRejection.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' has failed validation. They have been denied authentication."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Listeners/LogAuthenticationSuccess.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' has been successfully logged in."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogDiscovery.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' has been successfully found for authentication."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogImport.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' is being imported."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogSynchronized.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' has been successfully synchronized."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogSynchronizing.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' is being synchronized."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogTrashedModel.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' was denied authentication because their model has been soft-deleted."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Listeners/LogWindowsAuth.php: -------------------------------------------------------------------------------- 1 | user->getCommonName()}' has successfully authenticated via NTLM."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Middleware/WindowsAuthenticate.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 38 | } 39 | 40 | /** 41 | * Handle an incoming request. 42 | * 43 | * @param Request $request 44 | * @param Closure $next 45 | * 46 | * @return mixed 47 | */ 48 | public function handle(Request $request, Closure $next) 49 | { 50 | if (! $this->auth->check()) { 51 | // Retrieve the users account name from the request. 52 | if ($account = $this->account($request)) { 53 | // Retrieve the users username from their account name. 54 | $username = $this->username($account); 55 | 56 | // Finally, retrieve the users authenticatable model and log them in. 57 | if ($user = $this->retrieveAuthenticatedUser($username)) { 58 | $this->auth->login($user, $remember = true); 59 | } 60 | } 61 | } 62 | 63 | return $next($request); 64 | } 65 | 66 | /** 67 | * Returns the authenticatable user instance if found. 68 | * 69 | * @param string $username 70 | * 71 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 72 | */ 73 | protected function retrieveAuthenticatedUser($username) 74 | { 75 | // Find the user in LDAP. 76 | if ($user = $this->resolveUserByUsername($username)) { 77 | $model = null; 78 | 79 | // If we are using the DatabaseUserProvider, we must locate or import 80 | // the users model that is currently authenticated with SSO. 81 | if ($this->auth->getProvider() instanceof DatabaseUserProvider) { 82 | // Here we will import the LDAP user. If the user already exists in 83 | // our local database, it will be returned from the importer. 84 | $model = Bus::dispatch( 85 | new Import($user, $this->model()) 86 | ); 87 | } 88 | 89 | // Here we will validate that the authenticating user 90 | // passes our LDAP authentication rules in place. 91 | if ($this->passesValidation($user, $model)) { 92 | if ($model) { 93 | // We will sync / set the users password after 94 | // our model has been synchronized. 95 | Bus::dispatch(new SyncPassword($model)); 96 | 97 | // We also want to save the model in case it doesn't 98 | // exist yet, or there are changes to be synced. 99 | $model->save(); 100 | } 101 | 102 | $this->fireAuthenticatedEvent($user, $model); 103 | 104 | return $model ? $model : $user; 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Fires the windows authentication event. 111 | * 112 | * @param User $user 113 | * @param mixed|null $model 114 | * 115 | * @return void 116 | */ 117 | protected function fireAuthenticatedEvent(User $user, $model = null) 118 | { 119 | Event::dispatch(new AuthenticatedWithWindows($user, $model)); 120 | } 121 | 122 | /** 123 | * Retrieves an LDAP user by their username. 124 | * 125 | * @param string $username 126 | * 127 | * @return mixed 128 | */ 129 | protected function resolveUserByUsername($username) 130 | { 131 | return Resolver::query()->whereEquals($this->discover(), $username)->first(); 132 | } 133 | 134 | /** 135 | * Returns the configured authentication model. 136 | * 137 | * @return \Illuminate\Database\Eloquent\Model 138 | */ 139 | protected function model() 140 | { 141 | return $this->auth->getProvider()->createModel(); 142 | } 143 | 144 | /** 145 | * Retrieves the users SSO account name from our server. 146 | * 147 | * @param Request $request 148 | * 149 | * @return string 150 | */ 151 | protected function account(Request $request) 152 | { 153 | return utf8_encode($request->server($this->key())); 154 | } 155 | 156 | /** 157 | * Retrieves the users username from their full account name. 158 | * 159 | * @param string $account 160 | * 161 | * @return string 162 | */ 163 | protected function username($account) 164 | { 165 | // Username's may be prefixed with their domain, 166 | // we just need their account name. 167 | $username = explode('\\', $account); 168 | 169 | if (count($username) === 2) { 170 | [$domain, $username] = $username; 171 | } else { 172 | $username = $username[key($username)]; 173 | } 174 | 175 | return $username; 176 | } 177 | 178 | /** 179 | * Returns the configured key to use for retrieving 180 | * the username from the server global variable. 181 | * 182 | * @return string 183 | */ 184 | protected function key() 185 | { 186 | return Config::get('ldap_auth.identifiers.windows.server_key', 'AUTH_USER'); 187 | } 188 | 189 | /** 190 | * Returns the attribute to discover users by. 191 | * 192 | * @return string 193 | */ 194 | protected function discover() 195 | { 196 | return Config::get('ldap_auth.identifiers.windows.locate_users_by', 'samaccountname'); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Resolvers/ResolverInterface.php: -------------------------------------------------------------------------------- 1 | ldap = $ldap; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function setConnection($connection) 49 | { 50 | $this->connection = $connection; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function byId($identifier) 57 | { 58 | if ($user = $this->query()->findByGuid($identifier)) { 59 | return $user; 60 | } 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function byCredentials(array $credentials = []) 67 | { 68 | if (empty($credentials)) { 69 | return; 70 | } 71 | 72 | // Depending on the configured user provider, the 73 | // username field will differ for retrieving 74 | // users by their credentials. 75 | $attribute = $this->getAppAuthProvider() instanceof NoDatabaseUserProvider ? 76 | $this->getLdapDiscoveryAttribute() : 77 | $this->getDatabaseUsernameColumn(); 78 | 79 | if (! array_key_exists($attribute, $credentials)) { 80 | throw new RuntimeException( 81 | "The '$attribute' key is missing from the given credentials array." 82 | ); 83 | } 84 | 85 | return $this->query()->whereEquals( 86 | $this->getLdapDiscoveryAttribute(), 87 | $credentials[$attribute] 88 | )->first(); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function byModel(Authenticatable $model) 95 | { 96 | return $this->byId($model->{$this->getDatabaseIdColumn()}); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function authenticate(User $user, array $credentials = []) 103 | { 104 | $attribute = $this->getLdapAuthAttribute(); 105 | 106 | // If the developer has inserted 'dn' as their LDAP 107 | // authentication attribute, we'll convert it to 108 | // the full attribute name for convenience. 109 | if ($attribute == 'dn') { 110 | $attribute = $user->getSchema()->distinguishedName(); 111 | } 112 | 113 | $username = $user->getFirstAttribute($attribute); 114 | 115 | $password = $this->getPasswordFromCredentials($credentials); 116 | 117 | Event::dispatch(new Authenticating($user, $username)); 118 | 119 | if ($this->getLdapAuthProvider()->auth()->attempt($username, $password)) { 120 | Event::dispatch(new Authenticated($user)); 121 | 122 | return true; 123 | } 124 | 125 | Event::dispatch(new AuthenticationFailed($user)); 126 | 127 | return false; 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function query(): Builder 134 | { 135 | $query = $this->getLdapAuthProvider()->search()->users(); 136 | 137 | // We will ensure our object GUID attribute is always selected 138 | // along will all attributes. Otherwise, if the object GUID 139 | // attribute is virtual, it may not be returned. 140 | $selects = array_unique(array_merge(['*', $query->getSchema()->objectGuid()], $query->getSelects())); 141 | 142 | $query->select($selects); 143 | 144 | foreach ($this->getQueryScopes() as $scope) { 145 | app($scope)->apply($query); 146 | } 147 | 148 | return $query; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function getLdapDiscoveryAttribute(): string 155 | { 156 | return Config::get('ldap_auth.identifiers.ldap.locate_users_by', 'userprincipalname'); 157 | } 158 | 159 | /** 160 | * {@inheritdoc} 161 | */ 162 | public function getLdapAuthAttribute(): string 163 | { 164 | return Config::get('ldap_auth.identifiers.ldap.bind_users_by', 'distinguishedname'); 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function getDatabaseUsernameColumn(): string 171 | { 172 | return Config::get('ldap_auth.identifiers.database.username_column', 'email'); 173 | } 174 | 175 | /** 176 | * {@inheritdoc} 177 | */ 178 | public function getDatabaseIdColumn(): string 179 | { 180 | return Config::get('ldap_auth.identifiers.database.guid_column', 'objectguid'); 181 | } 182 | 183 | /** 184 | * Returns the password field to retrieve from the credentials. 185 | * 186 | * @param array $credentials 187 | * 188 | * @return string|null 189 | */ 190 | protected function getPasswordFromCredentials($credentials) 191 | { 192 | return Arr::get($credentials, 'password'); 193 | } 194 | 195 | /** 196 | * Retrieves the provider for the current connection. 197 | * 198 | * @throws \Adldap\AdldapException 199 | * 200 | * @return ProviderInterface 201 | */ 202 | protected function getLdapAuthProvider(): ProviderInterface 203 | { 204 | $provider = $this->ldap->getProvider($this->connection ?? $this->getLdapAuthConnectionName()); 205 | 206 | if (! $provider->getConnection()->isBound()) { 207 | // We'll make sure we have a bound connection before 208 | // allowing dynamic calls on the default provider. 209 | $provider->connect(); 210 | } 211 | 212 | return $provider; 213 | } 214 | 215 | /** 216 | * Returns the default guards provider instance. 217 | * 218 | * @return UserProvider 219 | */ 220 | protected function getAppAuthProvider(): UserProvider 221 | { 222 | return Auth::guard()->getProvider(); 223 | } 224 | 225 | /** 226 | * Returns the connection name of the authentication provider. 227 | * 228 | * @return string 229 | */ 230 | protected function getLdapAuthConnectionName() 231 | { 232 | return Config::get('ldap_auth.connection', 'default'); 233 | } 234 | 235 | /** 236 | * Returns the configured query scopes. 237 | * 238 | * @return array 239 | */ 240 | protected function getQueryScopes() 241 | { 242 | return Config::get('ldap_auth.scopes', []); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Scopes/MemberOfScope.php: -------------------------------------------------------------------------------- 1 | select($this->getSelectedAttributes($builder)); 15 | } 16 | 17 | /** 18 | * Retrieve the attributes to select for the scope. 19 | * 20 | * This merges the current queries selected attributes so we 21 | * don't overwrite any other scopes selected attributes. 22 | * 23 | * @param Builder $builder 24 | * 25 | * @return array 26 | */ 27 | protected function getSelectedAttributes(Builder $builder) 28 | { 29 | $selected = $builder->getSelects(); 30 | 31 | return array_merge($selected, [ 32 | $builder->getSchema()->memberOf(), 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Scopes/ScopeInterface.php: -------------------------------------------------------------------------------- 1 | whereHas($builder->getSchema()->userId()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Scopes/UpnScope.php: -------------------------------------------------------------------------------- 1 | whereHas($builder->getSchema()->userPrincipalName()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Traits/HasLdapUser.php: -------------------------------------------------------------------------------- 1 | ldap = $user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/ValidatesUsers.php: -------------------------------------------------------------------------------- 1 | rules($user, $model) 24 | ))->passes(); 25 | } 26 | 27 | /** 28 | * Returns an array of constructed rules. 29 | * 30 | * @param User $user 31 | * @param Model|null $model 32 | * 33 | * @return array 34 | */ 35 | protected function rules(User $user, Model $model = null) 36 | { 37 | $rules = []; 38 | 39 | foreach ($this->getRules() as $rule) { 40 | $rules[] = new $rule($user, $model); 41 | } 42 | 43 | return $rules; 44 | } 45 | 46 | /** 47 | * Retrieves the configured authentication rules. 48 | * 49 | * @return array 50 | */ 51 | protected function getRules() 52 | { 53 | return Config::get('ldap_auth.rules', []); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Validation/Rules/DenyTrashed.php: -------------------------------------------------------------------------------- 1 | isTrashed()) { 16 | Event::dispatch( 17 | new AuthenticatedModelTrashed($this->user, $this->model) 18 | ); 19 | 20 | return false; 21 | } 22 | 23 | return true; 24 | } 25 | 26 | /** 27 | * Determines if the current model is trashed. 28 | * 29 | * @return bool 30 | */ 31 | protected function isTrashed() 32 | { 33 | return $this->model 34 | ? method_exists($this->model, 'trashed') && $this->model->trashed() 35 | : false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Validation/Rules/OnlyImported.php: -------------------------------------------------------------------------------- 1 | model && $this->model->exists; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Validation/Rules/Rule.php: -------------------------------------------------------------------------------- 1 | user = $user; 33 | $this->model = $model; 34 | } 35 | 36 | /** 37 | * Checks if the rule passes validation. 38 | * 39 | * @return bool 40 | */ 41 | abstract public function isValid(); 42 | } 43 | -------------------------------------------------------------------------------- /src/Validation/Validator.php: -------------------------------------------------------------------------------- 1 | addRule($rule); 25 | } 26 | } 27 | 28 | /** 29 | * Checks if each rule passes validation. 30 | * 31 | * If all rules pass, authentication is granted. 32 | * 33 | * @return bool 34 | */ 35 | public function passes() 36 | { 37 | foreach ($this->rules as $rule) { 38 | if (! $rule->isValid()) { 39 | return false; 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** 47 | * Checks if a rule fails validation. 48 | * 49 | * @return bool 50 | */ 51 | public function fails() 52 | { 53 | return ! $this->passes(); 54 | } 55 | 56 | /** 57 | * Adds a rule to the validator. 58 | * 59 | * @param Rule $rule 60 | */ 61 | public function addRule(Rule $rule) 62 | { 63 | $this->rules[] = $rule; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Console/ImportTest.php: -------------------------------------------------------------------------------- 1 | makeLdapUser(); 20 | 21 | $b->shouldReceive('findOrFail')->once()->with('jdoe')->andReturn($u); 22 | 23 | Resolver::shouldReceive('query')->once()->andReturn($b) 24 | ->shouldReceive('getDatabaseIdColumn')->twice()->andReturn('objectguid') 25 | ->shouldReceive('getDatabaseUsernameColumn')->once()->andReturn('email') 26 | ->shouldReceive('getLdapDiscoveryAttribute')->once()->andReturn('userprincipalname'); 27 | 28 | $this->artisan('adldap:import', ['user' => 'jdoe', '--no-interaction' => true]) 29 | ->expectsOutput("Found user 'John Doe'.") 30 | ->expectsOutput('Successfully imported / synchronized 1 user(s).') 31 | ->assertExitCode(0); 32 | 33 | $this->assertDatabaseHas('users', ['email' => 'jdoe@email.com']); 34 | } 35 | 36 | public function test_importing_multiple_users() 37 | { 38 | $b = m::mock(Builder::class); 39 | 40 | $users = [ 41 | $this->makeLdapUser([ 42 | 'objectguid' => ['cc07cacc-5d9d-fa40-a9fb-3a4d50a172b0'], 43 | 'samaccountname' => ['johndoe'], 44 | 'userprincipalname' => ['johndoe@email.com'], 45 | 'mail' => ['johndoe@email.com'], 46 | 'cn' => ['John Doe'], 47 | ]), 48 | $this->makeLdapUser([ 49 | 'objectguid' => ['cc07cacc-5d9d-fa40-a9fb-3a4d50a172b1'], 50 | 'samaccountname' => ['janedoe'], 51 | 'userprincipalname' => ['janedoe@email.com'], 52 | 'mail' => ['janedoe@email.com'], 53 | 'cn' => ['Jane Doe'], 54 | ]), 55 | ]; 56 | 57 | $b->shouldReceive('paginate')->once()->andReturn($b) 58 | ->shouldReceive('getResults')->once()->andReturn($users); 59 | 60 | Resolver::shouldReceive('query')->once()->andReturn($b) 61 | ->shouldReceive('getDatabaseIdColumn')->times(4)->andReturn('objectguid') 62 | ->shouldReceive('getDatabaseUsernameColumn')->twice()->andReturn('email') 63 | ->shouldReceive('getLdapDiscoveryAttribute')->twice()->andReturn('userprincipalname'); 64 | 65 | $this->artisan('adldap:import', ['--no-interaction' => true]) 66 | ->expectsOutput('Found 2 user(s).') 67 | ->expectsOutput('Successfully imported / synchronized 2 user(s).') 68 | ->assertExitCode(0); 69 | 70 | $this->assertDatabaseHas('users', ['email' => 'johndoe@email.com']); 71 | $this->assertDatabaseHas('users', ['email' => 'janedoe@email.com']); 72 | } 73 | 74 | public function test_questions_asked_with_interaction() 75 | { 76 | $b = m::mock(Builder::class); 77 | 78 | $u = $this->makeLdapUser(); 79 | 80 | $b->shouldReceive('findOrFail')->once()->with('jdoe')->andReturn($u); 81 | 82 | Resolver::shouldReceive('query')->once()->andReturn($b) 83 | ->shouldReceive('getDatabaseIdColumn')->twice()->andReturn('objectguid') 84 | ->shouldReceive('getDatabaseUsernameColumn')->once()->andReturn('email') 85 | ->shouldReceive('getLdapDiscoveryAttribute')->once()->andReturn('userprincipalname'); 86 | 87 | $this->artisan('adldap:import', ['user' => 'jdoe']) 88 | ->expectsOutput("Found user 'John Doe'.") 89 | ->expectsQuestion('Would you like to display the user(s) to be imported / synchronized?', 'no') 90 | ->expectsQuestion('Would you like these users to be imported / synchronized?', 'yes') 91 | ->expectsOutput('Successfully imported / synchronized 1 user(s).') 92 | ->assertExitCode(0); 93 | 94 | $this->assertDatabaseHas('users', ['email' => 'jdoe@email.com']); 95 | } 96 | 97 | public function test_model_will_be_restored_when_ldap_account_is_active() 98 | { 99 | $user = $this->makeLdapUser(); 100 | 101 | $model = TestUser::create([ 102 | 'objectguid' => $user->getConvertedGuid(), 103 | 'email' => $user->getUserPrincipalName(), 104 | 'name' => $user->getCommonName(), 105 | 'password' => Hash::make('password'), 106 | ]); 107 | 108 | $model->delete(); 109 | 110 | $this->assertTrue($model->trashed()); 111 | 112 | $user->setUserAccountControl((new AccountControl())->accountIsNormal()); 113 | 114 | $this->assertTrue($user->isEnabled()); 115 | 116 | $b = m::mock(Builder::class); 117 | 118 | $b->shouldReceive('paginate')->once()->andReturn($b) 119 | ->shouldReceive('getResults')->once()->andReturn([$user]); 120 | 121 | Resolver::shouldReceive('query')->once()->andReturn($b) 122 | ->shouldReceive('getDatabaseIdColumn')->twice()->andReturn('objectguid') 123 | ->shouldReceive('getDatabaseUsernameColumn')->once()->andReturn('email') 124 | ->shouldReceive('getLdapDiscoveryAttribute')->once()->andReturn('userprincipalname'); 125 | 126 | $this->artisan('adldap:import', ['--restore' => true, '--no-interaction' => true]) 127 | ->expectsOutput("Found user 'John Doe'.") 128 | ->expectsOutput('Successfully imported / synchronized 1 user(s).') 129 | ->assertExitCode(0); 130 | 131 | $this->assertFalse($model->fresh()->trashed()); 132 | } 133 | 134 | public function test_model_will_be_soft_deleted_when_ldap_account_is_disabled() 135 | { 136 | $user = $this->makeLdapUser(); 137 | 138 | $user->setUserAccountControl((new AccountControl())->accountIsDisabled()); 139 | 140 | $this->assertTrue($user->isDisabled()); 141 | 142 | $model = TestUser::create([ 143 | 'objectguid' => $user->getConvertedGuid(), 144 | 'email' => 'jdoe@email.com', 145 | 'name' => 'John Doe', 146 | 'password' => Hash::make('password'), 147 | ]); 148 | 149 | $this->assertFalse($model->trashed()); 150 | 151 | $b = m::mock(Builder::class); 152 | 153 | $b->shouldReceive('paginate')->once()->andReturn($b) 154 | ->shouldReceive('getResults')->once()->andReturn([$user]); 155 | 156 | Resolver::shouldReceive('query')->once()->andReturn($b) 157 | ->shouldReceive('getDatabaseIdColumn')->twice()->andReturn('objectguid') 158 | ->shouldReceive('getDatabaseUsernameColumn')->once()->andReturn('email') 159 | ->shouldReceive('getLdapDiscoveryAttribute')->once()->andReturn('userprincipalname'); 160 | 161 | $this->artisan('adldap:import', ['--delete' => true, '--no-interaction' => true]) 162 | ->expectsOutput("Found user 'John Doe'.") 163 | ->expectsOutput('Successfully imported / synchronized 1 user(s).') 164 | ->assertExitCode(0); 165 | 166 | $this->assertTrue($model->fresh()->trashed()); 167 | } 168 | 169 | public function test_filter_option_applies_to_ldap_query() 170 | { 171 | $f = '(samaccountname=jdoe)'; 172 | 173 | $b = m::mock(Builder::class); 174 | 175 | $b 176 | ->shouldReceive('rawFilter')->once()->with($f)->andReturnSelf() 177 | ->shouldReceive('paginate')->once()->andReturnSelf() 178 | ->shouldReceive('getResults')->once()->andReturn([]); 179 | 180 | Resolver::shouldReceive('query')->once()->andReturn($b); 181 | 182 | $this->artisan('adldap:import', ['--filter' => $f, '--no-interaction' => true]) 183 | ->expectsOutput('There were no users found to import.'); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tests/DatabaseImporterTest.php: -------------------------------------------------------------------------------- 1 | makeLdapUser(); 15 | 16 | $importer = new Import($user, new TestUser()); 17 | 18 | $model = $importer->handle(); 19 | 20 | $this->assertEquals($user->getCommonName(), $model->name); 21 | $this->assertEquals($user->getUserPrincipalName(), $model->email); 22 | $this->assertFalse($model->exists); 23 | } 24 | 25 | /** @test */ 26 | public function ldap_users_are_not_duplicated_with_alternate_casing() 27 | { 28 | $firstUser = $this->makeLdapUser(); 29 | 30 | $firstUser->setUserPrincipalName('JDOE@EMAIL.com'); 31 | 32 | $m1 = (new Import($firstUser, new TestUser()))->handle(); 33 | 34 | $m1->password = bcrypt(Str::random(16)); 35 | 36 | $m1->save(); 37 | 38 | $secondUser = $this->makeLdapUser(); 39 | 40 | $secondUser->setUserPrincipalName('jdoe@email.com'); 41 | 42 | $m2 = (new Import($secondUser, new TestUser()))->handle(); 43 | 44 | $this->assertTrue($m1->is($m2)); 45 | } 46 | 47 | /** 48 | * @test 49 | * @expectedException \UnexpectedValueException 50 | */ 51 | public function exception_is_thrown_when_guid_is_null() 52 | { 53 | $u = $this->makeLdapUser([ 54 | 'objectguid' => null, 55 | ]); 56 | 57 | (new Import($u, new TestUser()))->handle(); 58 | } 59 | 60 | /** 61 | * @test 62 | * @expectedException \UnexpectedValueException 63 | */ 64 | public function exception_is_thrown_when_guid_is_empty() 65 | { 66 | $u = $this->makeLdapUser([ 67 | 'objectguid' => ' ', 68 | ]); 69 | 70 | (new Import($u, new TestUser()))->handle(); 71 | } 72 | 73 | /** 74 | * @test 75 | * @expectedException \UnexpectedValueException 76 | */ 77 | public function exception_is_thrown_when_username_is_null() 78 | { 79 | $u = $this->makeLdapUser([ 80 | 'userprincipalname' => null, 81 | ]); 82 | 83 | (new Import($u, new TestUser()))->handle(); 84 | } 85 | 86 | /** 87 | * @test 88 | * @expectedException \UnexpectedValueException 89 | */ 90 | public function exception_is_thrown_when_username_is_empty() 91 | { 92 | $u = $this->makeLdapUser([ 93 | 'userprincipalname' => ' ', 94 | ]); 95 | 96 | (new Import($u, new TestUser()))->handle(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/DatabaseTestCase.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('name'); 18 | $table->string('email')->unique(); 19 | $table->string('objectguid')->unique()->nullable(); 20 | $table->string('password', 60); 21 | $table->rememberToken(); 22 | $table->timestamps(); 23 | $table->softDeletes(); 24 | }); 25 | 26 | Hash::setRounds(4); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/EloquentAuthenticateTest.php: -------------------------------------------------------------------------------- 1 | app['config']->set('auth.guards.web.provider', 'users'); 16 | 17 | $user = $this->makeLdapUser([ 18 | 'objectguid' => ['cc07cacc-5d9d-fa40-a9fb-3a4d50a172b0'], 19 | 'cn' => ['John Doe'], 20 | 'userprincipalname' => ['jdoe@email.com'], 21 | ]); 22 | 23 | $importer = new Import($user, new TestUser()); 24 | 25 | $model = $importer->handle(); 26 | 27 | Resolver::spy(); 28 | Resolver::shouldNotReceive('byModel'); 29 | 30 | Auth::login($model); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Handlers/LdapAttributeHandler.php: -------------------------------------------------------------------------------- 1 | name = 'handled'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/HasLdapUserTest.php: -------------------------------------------------------------------------------- 1 | createEloquentUser(); 16 | 17 | $ldapUser = m::mock(LdapUser::class); 18 | 19 | $user->setLdapUser($ldapUser); 20 | 21 | $this->assertEquals($ldapUser, $user->ldap); 22 | } 23 | 24 | /** @test */ 25 | public function null_ldap_user_can_be_given() 26 | { 27 | $user = $this->createEloquentUser(); 28 | 29 | $user->setLdapUser(null); 30 | 31 | $this->assertNull($user->ldap); 32 | } 33 | 34 | /** 35 | * @return HasLdapUser 36 | */ 37 | protected function createEloquentUser() 38 | { 39 | $user = new EloquentUser(); 40 | 41 | if (! array_key_exists(HasLdapUser::class, class_uses(EloquentUser::class))) { 42 | $this->fail('TestUser model does not use '.HasLdapUser::class); 43 | } 44 | 45 | return $user; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Listeners/LogAuthenticatedTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn('jdoe'); 22 | 23 | $e = new Authenticated($user); 24 | 25 | $logged = "User 'jdoe' has successfully passed LDAP authentication."; 26 | 27 | Log::shouldReceive('info')->once()->with($logged); 28 | 29 | $l->handle($e); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Listeners/LogAuthenticationFailureTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 24 | 25 | $e = new AuthenticationFailed($user); 26 | 27 | $logged = "User '{$name}' has failed LDAP authentication."; 28 | 29 | Log::shouldReceive('info')->once()->with($logged); 30 | 31 | $l->handle($e); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Listeners/LogAuthenticationRejectionTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 24 | 25 | $e = new AuthenticationRejected($user); 26 | 27 | $logged = "User '{$name}' has failed validation. They have been denied authentication."; 28 | 29 | Log::shouldReceive('info')->once()->with($logged); 30 | 31 | $l->handle($e); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Listeners/LogAuthenticationSuccessTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 24 | 25 | $e = new AuthenticationSuccessful($user); 26 | 27 | $logged = "User '{$name}' has been successfully logged in."; 28 | 29 | Log::shouldReceive('info')->once()->with($logged); 30 | 31 | $l->handle($e); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Listeners/LogAuthenticationTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 25 | 26 | $username = 'jdoe'; 27 | $prefix = 'prefix.'; 28 | $suffix = '.suffix'; 29 | 30 | $authUsername = $prefix.$username.$suffix; 31 | 32 | $e = new Authenticating($user, $username); 33 | 34 | $logged = "User '{$name}' is authenticating with username: '{$authUsername}'"; 35 | 36 | Log::shouldReceive('info')->once()->with($logged); 37 | 38 | Config::shouldReceive('get')->with('ldap_auth.connection')->andReturn('default') 39 | ->shouldReceive('get')->with('ldap.connections.default.settings.account_prefix')->andReturn($prefix) 40 | ->shouldReceive('get')->with('ldap.connections.default.settings.account_suffix')->andReturn($suffix); 41 | 42 | $l->handle($e); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Listeners/LogDiscoveryTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 24 | 25 | $e = new DiscoveredWithCredentials($user); 26 | 27 | $logged = "User '{$name}' has been successfully found for authentication."; 28 | 29 | Log::shouldReceive('info')->once()->with($logged); 30 | 31 | $l->handle($e); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Listeners/LogImportTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 26 | 27 | $e = new Importing($user, $model); 28 | 29 | $logged = "User '{$name}' is being imported."; 30 | 31 | Log::shouldReceive('info')->once()->with($logged); 32 | 33 | $l->handle($e); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Listeners/LogSynchronizedTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 26 | 27 | $e = new Synchronized($user, $model); 28 | 29 | $logged = "User '{$name}' has been successfully synchronized."; 30 | 31 | Log::shouldReceive('info')->once()->with($logged); 32 | 33 | $l->handle($e); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Listeners/LogSynchronizingTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 26 | 27 | $e = new Synchronizing($user, $model); 28 | 29 | $logged = "User '{$name}' is being synchronized."; 30 | 31 | Log::shouldReceive('info')->once()->with($logged); 32 | 33 | $l->handle($e); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Listeners/LogTrashedModelTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 25 | 26 | $e = new AuthenticatedModelTrashed($user, m::mock(Authenticatable::class)); 27 | 28 | $logged = "User '{$name}' was denied authentication because their model has been soft-deleted."; 29 | 30 | Log::shouldReceive('info')->once()->with($logged); 31 | 32 | $l->handle($e); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Listeners/LogWindowsAuthTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getCommonName')->andReturn($name); 25 | 26 | $e = new AuthenticatedWithWindows($user, m::mock(Authenticatable::class)); 27 | 28 | $logged = "User '{$name}' has successfully authenticated via NTLM."; 29 | 30 | Log::shouldReceive('info')->once()->with($logged); 31 | 32 | $l->handle($e); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Models/TestUser.php: -------------------------------------------------------------------------------- 1 | 'jdoe@email.com', 16 | 'password' => '12345', 17 | ]; 18 | 19 | $user = $this->makeLdapUser(); 20 | 21 | Resolver::shouldReceive('byCredentials')->once()->andReturn($user) 22 | ->shouldReceive('authenticate')->once()->withArgs([$user, $credentials])->andReturn(true); 23 | 24 | $this->assertTrue(Auth::attempt($credentials)); 25 | 26 | $user = Auth::user(); 27 | 28 | $this->assertInstanceOf(User::class, $user); 29 | $this->assertEquals($credentials['email'], $user->mail[0]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/NoDatabaseTestCase.php: -------------------------------------------------------------------------------- 1 | set('ldap.connections.default.auto_connect', false); 20 | $app['config']->set('ldap.connections.default.connection', Ldap::class); 21 | $app['config']->set('ldap.connections.default.settings', [ 22 | 'username' => 'admin', 23 | 'password' => 'password', 24 | 'schema' => ActiveDirectory::class, 25 | ]); 26 | 27 | // Adldap auth setup. 28 | $app['config']->set('ldap_auth.provider', NoDatabaseUserProvider::class); 29 | 30 | // Laravel auth setup. 31 | $app['config']->set('auth.guards.web.provider', 'ldap'); 32 | $app['config']->set('auth.providers', [ 33 | 'ldap' => [ 34 | 'driver' => 'ldap', 35 | ], 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Scopes/JohnDoeScope.php: -------------------------------------------------------------------------------- 1 | whereCn('John Doe'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 45 | $config->set('database.connections.testbench', [ 46 | 'driver' => 'sqlite', 47 | 'database' => ':memory:', 48 | 'prefix' => '', 49 | ]); 50 | 51 | // Adldap connection set$configup. 52 | $config->set('ldap.connections.default.auto_connect', false); 53 | $config->set('ldap.connections.default.connection', Ldap::class); 54 | $config->set('ldap.connections.default.settings', [ 55 | 'username' => 'admin@email.com', 56 | 'password' => 'password', 57 | 'schema' => ActiveDirectory::class, 58 | ]); 59 | 60 | // Adldap auth setup. 61 | $config->set('ldap_auth.provider', DatabaseUserProvider::class); 62 | $config->set('ldap_auth.sync_attributes', [ 63 | 'email' => 'userprincipalname', 64 | 'name' => 'cn', 65 | ]); 66 | 67 | // Laravel auth setup. 68 | $config->set('auth.guards.web.provider', 'ldap'); 69 | $config->set('auth.providers', [ 70 | 'ldap' => [ 71 | 'driver' => 'ldap', 72 | 'model' => TestUser::class, 73 | ], 74 | 'users' => [ 75 | 'driver' => 'eloquent', 76 | 'model' => TestUser::class, 77 | ], 78 | ]); 79 | } 80 | 81 | /** 82 | * Returns a new LDAP user model. 83 | * 84 | * @param array $attributes 85 | * 86 | * @return \Adldap\Models\User 87 | */ 88 | protected function makeLdapUser(array $attributes = []) 89 | { 90 | return Adldap::getDefaultProvider()->make()->user($attributes ?: [ 91 | 'objectguid' => ['cc07cacc-5d9d-fa40-a9fb-3a4d50a172b0'], 92 | 'samaccountname' => ['jdoe'], 93 | 'userprincipalname' => ['jdoe@email.com'], 94 | 'mail' => ['jdoe@email.com'], 95 | 'cn' => ['John Doe'], 96 | ]); 97 | } 98 | 99 | /** 100 | * Returns a mock LDAP connection object. 101 | * 102 | * @param array $methods 103 | * 104 | * @return \PHPUnit\Framework\MockObject\MockObject 105 | */ 106 | protected function getMockConnection($methods = []) 107 | { 108 | $defaults = ['isBound', 'search', 'getEntries', 'bind', 'close']; 109 | 110 | $connection = $this->getMockBuilder(Ldap::class) 111 | ->setMethods(array_merge($defaults, $methods)) 112 | ->getMock(); 113 | 114 | Adldap::getDefaultProvider()->setConnection($connection); 115 | 116 | return $connection; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/UserResolverTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('email', $resolver->getDatabaseUsernameColumn()); 31 | } 32 | 33 | /** @test */ 34 | public function ldap_auth_username_default() 35 | { 36 | $ldap = m::mock(AdldapInterface::class); 37 | 38 | $resolver = new UserResolver($ldap); 39 | 40 | $this->assertEquals('distinguishedname', $resolver->getLdapAuthAttribute()); 41 | } 42 | 43 | /** @test */ 44 | public function ldap_username_default() 45 | { 46 | $ldap = m::mock(AdldapInterface::class); 47 | 48 | $resolver = new UserResolver($ldap); 49 | 50 | $this->assertEquals('userprincipalname', $resolver->getLdapDiscoveryAttribute()); 51 | } 52 | 53 | /** @test */ 54 | public function by_credentials_returns_null_on_empty_credentials() 55 | { 56 | $ldap = m::mock(AdldapInterface::class); 57 | 58 | $resolver = new UserResolver($ldap); 59 | 60 | $this->assertNull($resolver->byCredentials()); 61 | } 62 | 63 | /** @test */ 64 | public function scopes_are_applied_when_query_is_called() 65 | { 66 | config(['ldap_auth.scopes' => [UpnScope::class]]); 67 | 68 | $schema = m::mock(SchemaInterface::class); 69 | 70 | $schema 71 | ->shouldReceive('userPrincipalName')->once()->withNoArgs()->andReturn('userprincipalname') 72 | ->shouldReceive('objectGuid')->once()->withNoArgs()->andReturn('objectguid'); 73 | 74 | $builder = m::mock(Builder::class); 75 | 76 | $builder 77 | ->shouldReceive('whereHas')->once()->withArgs(['userprincipalname'])->andReturnSelf() 78 | ->shouldReceive('getSelects')->once()->andReturn(['*']) 79 | ->shouldReceive('select')->with(['*', 'objectguid'])->andReturnSelf() 80 | ->shouldReceive('getSchema')->twice()->andReturn($schema); 81 | 82 | $provider = m::mock(ProviderInterface::class); 83 | 84 | $provider 85 | ->shouldReceive('search')->once()->andReturn($provider) 86 | ->shouldReceive('users')->once()->andReturn($builder); 87 | 88 | $ad = m::mock(AdldapInterface::class); 89 | $ldapConnection = m::mock(ConnectionInterface::class); 90 | $ldapConnection->shouldReceive('isBound')->once()->andReturn(false); 91 | 92 | $provider->shouldReceive('getConnection')->once()->andReturn($ldapConnection); 93 | $provider->shouldReceive('connect')->once(); 94 | 95 | $ad->shouldReceive('getProvider')->with('default')->andReturn($provider); 96 | 97 | $resolver = new UserResolver($ad); 98 | 99 | $this->assertInstanceOf(Builder::class, $resolver->query()); 100 | } 101 | 102 | /** @test */ 103 | public function connection_is_set_when_retrieving_provider() 104 | { 105 | Config::shouldReceive('get')->once()->with('ldap_auth.connection', 'default')->andReturn('other-domain'); 106 | 107 | $ad = m::mock(AdldapInterface::class); 108 | $provider = m::mock(ProviderInterface::class); 109 | 110 | $ad->shouldReceive('getProvider')->with('other-domain')->andReturn($provider); 111 | $ldapConnection = m::mock(ConnectionInterface::class); 112 | $ldapConnection->shouldReceive('isBound')->once()->andReturn(false); 113 | 114 | $provider->shouldReceive('getConnection')->once()->andReturn($ldapConnection); 115 | $provider->shouldReceive('connect')->once(); 116 | 117 | $r = m::mock(UserResolver::class, [$ad])->makePartial(); 118 | 119 | $r->getLdapAuthProvider(); 120 | } 121 | 122 | /** @test */ 123 | public function by_credentials_retrieves_alternate_username_attribute_depending_on_user_provider() 124 | { 125 | $schema = m::mock(SchemaInterface::class); 126 | 127 | $schema->shouldReceive('objectGuid')->once()->withNoArgs()->andReturn('objectguid'); 128 | 129 | $query = m::mock(Builder::class); 130 | 131 | $query 132 | ->shouldReceive('whereEquals')->once()->with('userprincipalname', 'jdoe')->andReturnSelf() 133 | ->shouldReceive('getSelects')->once()->andReturn(['*']) 134 | ->shouldReceive('select')->with(['*', 'objectguid'])->andReturnSelf() 135 | ->shouldReceive('getSchema')->once()->andReturn($schema) 136 | ->shouldReceive('first')->andReturnNull(); 137 | 138 | $ldapProvider = m::mock(ProviderInterface::class); 139 | 140 | $ldapProvider 141 | ->shouldReceive('search')->once()->andReturnSelf() 142 | ->shouldReceive('users')->once()->andReturn($query); 143 | 144 | $ad = m::mock(AdldapInterface::class); 145 | $ldapConnection = m::mock(ConnectionInterface::class); 146 | $ldapConnection->shouldReceive('isBound')->once()->andReturn(false); 147 | 148 | $ldapProvider->shouldReceive('getConnection')->once()->andReturn($ldapConnection); 149 | $ldapProvider->shouldReceive('connect')->once(); 150 | 151 | $ad->shouldReceive('getProvider')->once()->andReturn($ldapProvider); 152 | 153 | $ad->shouldReceive('getProvider')->andReturnSelf(); 154 | 155 | $authProvider = m::mock(NoDatabaseUserProvider::class); 156 | 157 | Auth::shouldReceive('guard')->once()->andReturnSelf()->shouldReceive('getProvider')->once()->andReturn($authProvider); 158 | 159 | Config::shouldReceive('get')->with('ldap_auth.connection', 'default')->andReturn('default') 160 | ->shouldReceive('get')->with('ldap_auth.identifiers.ldap.locate_users_by', 'userprincipalname')->andReturn('userprincipalname') 161 | ->shouldReceive('get')->with('ldap_auth.scopes', [])->andReturn([]); 162 | 163 | $resolver = new UserResolver($ad); 164 | 165 | $resolver->byCredentials([ 166 | 'userprincipalname' => 'jdoe', 167 | 'password' => 'Password1', 168 | ]); 169 | } 170 | 171 | /** @test */ 172 | public function by_id_retrieves_user_by_object_guid() 173 | { 174 | $user = $this->makeLdapUser(); 175 | 176 | $guid = $this->faker->uuid; 177 | 178 | $query = m::mock(Builder::class); 179 | 180 | $query->shouldReceive('findByGuid')->once()->with($guid)->andReturn($user); 181 | 182 | $r = m::mock(UserResolver::class)->makePartial(); 183 | 184 | $r->shouldReceive('query')->andReturn($query); 185 | 186 | $this->assertEquals($user, $r->byId($guid)); 187 | } 188 | 189 | /** @test */ 190 | public function by_model_retrieves_user_by_models_object_guid() 191 | { 192 | $model = new TestUser([ 193 | 'objectguid' => $this->faker->uuid, 194 | ]); 195 | 196 | $user = $this->makeLdapUser(); 197 | 198 | $query = m::mock(Builder::class); 199 | 200 | $query->shouldReceive('findByGuid')->once()->with($model->objectguid)->andReturn($user); 201 | 202 | $r = m::mock(UserResolver::class)->makePartial(); 203 | 204 | $r->shouldReceive('query')->andReturn($query); 205 | 206 | $this->assertEquals($user, $r->byModel($model)); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /tests/WindowsAuthenticateTest.php: -------------------------------------------------------------------------------- 1 | server->set('AUTH_USER', 'jdoe'); 20 | 21 | $user = $this->makeLdapUser([ 22 | 'objectguid' => ['cc07cacc-5d9d-fa40-a9fb-3a4d50a172b0'], 23 | 'cn' => ['John Doe'], 24 | 'userprincipalname' => ['jdoe@email.com'], 25 | 'samaccountname' => ['jdoe'], 26 | ]); 27 | 28 | $query = m::mock(Builder::class); 29 | 30 | $query 31 | ->shouldReceive('whereEquals')->once()->withArgs(['samaccountname', 'jdoe'])->andReturn($query) 32 | ->shouldReceive('first')->once()->andReturn($user); 33 | 34 | Resolver::shouldReceive('query')->once()->andReturn($query) 35 | ->shouldReceive('getDatabaseIdColumn')->twice()->andReturn('objectguid') 36 | ->shouldReceive('getDatabaseUsernameColumn')->once()->andReturn('email') 37 | ->shouldReceive('getLdapDiscoveryAttribute')->once()->andReturn('userprincipalname') 38 | ->shouldReceive('byModel')->once()->andReturn($user); 39 | 40 | app(WindowsAuthenticate::class)->handle($request, function () { 41 | }); 42 | 43 | $authenticated = auth()->user(); 44 | 45 | $this->assertEquals($user, $authenticated->ldap); 46 | $this->assertEquals('John Doe', $authenticated->name); 47 | $this->assertEquals('jdoe@email.com', $authenticated->email); 48 | $this->assertNotEmpty($authenticated->remember_token); 49 | } 50 | 51 | /** @test */ 52 | public function middleware_continues_request_when_user_is_not_found() 53 | { 54 | $request = app('request'); 55 | 56 | $request->server->set('AUTH_USER', 'jdoe'); 57 | 58 | $query = m::mock(Builder::class); 59 | 60 | $query 61 | ->shouldReceive('whereEquals')->once()->withArgs(['samaccountname', 'jdoe'])->andReturn($query) 62 | ->shouldReceive('first')->once()->andReturn(null); 63 | 64 | Resolver::shouldReceive('query')->once()->andReturn($query); 65 | 66 | app(WindowsAuthenticate::class)->handle($request, function () { 67 | }); 68 | 69 | $this->assertNull(auth()->user()); 70 | } 71 | 72 | /** @test */ 73 | public function middleware_validates_authenticating_users() 74 | { 75 | // Deny deleted users from authenticating. 76 | config()->set('ldap_auth.rules', [DenyTrashed::class]); 77 | 78 | // Create the deleted user. 79 | tap(new TestUser(), function ($user) { 80 | $user->name = 'John Doe'; 81 | $user->email = 'jdoe@email.com'; 82 | $user->password = 'secret'; 83 | $user->deleted_at = now(); 84 | 85 | $user->save(); 86 | }); 87 | 88 | $request = app('request'); 89 | 90 | $request->server->set('AUTH_USER', 'jdoe'); 91 | 92 | $user = $this->makeLdapUser([ 93 | 'objectguid' => ['cc07cacc-5d9d-fa40-a9fb-3a4d50a172b0'], 94 | 'cn' => ['John Doe'], 95 | 'userprincipalname' => ['jdoe@email.com'], 96 | 'samaccountname' => ['jdoe'], 97 | ]); 98 | 99 | $query = m::mock(Builder::class); 100 | 101 | $query 102 | ->shouldReceive('whereEquals')->once()->withArgs(['samaccountname', 'jdoe'])->andReturn($query) 103 | ->shouldReceive('first')->once()->andReturn($user); 104 | 105 | Resolver::shouldReceive('query')->once()->andReturn($query) 106 | ->shouldReceive('getDatabaseIdColumn')->twice()->andReturn('objectguid') 107 | ->shouldReceive('getDatabaseUsernameColumn')->once()->andReturn('email') 108 | ->shouldReceive('getLdapDiscoveryAttribute')->once()->andReturn('userprincipalname'); 109 | 110 | app(WindowsAuthenticate::class)->handle($request, function () { 111 | }); 112 | 113 | $this->assertNull(auth()->user()); 114 | } 115 | } 116 | --------------------------------------------------------------------------------