├── .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 |
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 |
--------------------------------------------------------------------------------