diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index c86e0d789..98ef67987 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Controllers\Auth; +use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Http\Request; use BookStack\Exceptions\SocialSignInException; use BookStack\Exceptions\UserRegistrationException; @@ -31,6 +32,8 @@ class AuthController extends Controller protected $redirectPath = '/'; protected $redirectAfterLogout = '/login'; + protected $username = 'email'; + protected $socialAuthService; protected $emailConfirmationService; @@ -48,6 +51,7 @@ class AuthController extends Controller $this->socialAuthService = $socialAuthService; $this->emailConfirmationService = $emailConfirmationService; $this->userRepo = $userRepo; + $this->username = config('auth.method') === 'standard' ? 'email' : 'username'; parent::__construct(); } @@ -104,6 +108,24 @@ class AuthController extends Controller return $this->registerUser($userData); } + + /** + * Overrides the action when a user is authenticated. + * If the user authenticated but does not exist in the user table we create them. + * @param Request $request + * @param Authenticatable $user + * @return \Illuminate\Http\RedirectResponse + */ + protected function authenticated(Request $request, Authenticatable $user) + { + if(!$user->exists) { + $user->save(); + $this->userRepo->attachDefaultRole($user); + auth()->login($user); + } + return redirect()->intended($this->redirectPath()); + } + /** * Register a new user after a registration callback. * @param $socialDriver @@ -232,7 +254,7 @@ class AuthController extends Controller public function getLogin() { $socialDrivers = $this->socialAuthService->getActiveDrivers(); - $authMethod = 'standard'; // TODO - rewrite to use config. + $authMethod = config('auth.method'); return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]); } diff --git a/app/Http/routes.php b/app/Http/routes.php index a8eb6b6e4..aedfca9bc 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -3,7 +3,7 @@ Route::get('/test', function() { // TODO - remove this $service = new \BookStack\Services\LdapService(); - $service->getUserDetails('ssmith'); + dd($service->getUserDetails('ksmith')); }); // Authenticated routes... diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 40e94b016..c027578a7 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -25,7 +25,7 @@ class AuthServiceProvider extends ServiceProvider public function register() { Auth::provider('ldap', function($app, array $config) { - return new LdapUserProvider($config['model']); + return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']); }); } } diff --git a/app/Providers/LdapUserProvider.php b/app/Providers/LdapUserProvider.php index c2b961a34..407791a7d 100644 --- a/app/Providers/LdapUserProvider.php +++ b/app/Providers/LdapUserProvider.php @@ -3,6 +3,8 @@ namespace BookStack\Providers; +use BookStack\Role; +use BookStack\Services\LdapService; use BookStack\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\UserProvider; @@ -17,14 +19,21 @@ class LdapUserProvider implements UserProvider */ protected $model; + /** + * @var LdapService + */ + protected $ldapService; + /** * LdapUserProvider constructor. - * @param $model + * @param $model + * @param LdapService $ldapService */ - public function __construct($model) + public function __construct($model, LdapService $ldapService) { $this->model = $model; + $this->ldapService = $ldapService; } /** @@ -34,8 +43,7 @@ class LdapUserProvider implements UserProvider */ public function createModel() { - $class = '\\'.ltrim($this->model, '\\'); - + $class = '\\' . ltrim($this->model, '\\'); return new $class; } @@ -55,7 +63,7 @@ class LdapUserProvider implements UserProvider * Retrieve a user by their unique identifier and "remember me" token. * * @param mixed $identifier - * @param string $token + * @param string $token * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByToken($identifier, $token) @@ -91,16 +99,21 @@ class LdapUserProvider implements UserProvider */ public function retrieveByCredentials(array $credentials) { - // TODO: Implement retrieveByCredentials() method. - // Get user via LDAP + $userDetails = $this->ldapService->getUserDetails($credentials['username']); + if ($userDetails === null) return null; // Search current user base by looking up a uid + $model = $this->createModel(); + $currentUser = $model->newQuery() + ->where('external_auth_id', $userDetails['uid']) + ->first(); - // If not exists create a new user instance with attached role - // but do not store it in the database yet + if ($currentUser !== null) return $currentUser; - // + $model->name = $userDetails['name']; + $model->external_auth_id = $userDetails['uid']; + return $model; } /** @@ -112,6 +125,6 @@ class LdapUserProvider implements UserProvider */ public function validateCredentials(Authenticatable $user, array $credentials) { - // TODO: Implement validateCredentials() method. + return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']); } } diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index fecd7c88b..88918910a 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -3,6 +3,7 @@ use BookStack\Role; use BookStack\User; +use Setting; class UserRepo { @@ -56,7 +57,7 @@ class UserRepo */ public function attachDefaultRole($user) { - $roleId = \Setting::get('registration-role'); + $roleId = Setting::get('registration-role'); if ($roleId === false) $roleId = $this->role->getDefault()->id; $user->attachRoleId($roleId); } diff --git a/app/Role.php b/app/Role.php index c698a1cf6..3d93bf770 100644 --- a/app/Role.php +++ b/app/Role.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model; class Role extends Model { /** - * Sets the default role name for newly registed users. + * Sets the default role name for newly registered users. * @var string */ protected static $default = 'viewer'; diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php index a540ab58b..bceed682a 100644 --- a/app/Services/LdapService.php +++ b/app/Services/LdapService.php @@ -2,18 +2,94 @@ use BookStack\Exceptions\LdapException; +use Illuminate\Contracts\Auth\Authenticatable; class LdapService { + protected $ldapConnection; + + /** + * Get the details of a user from LDAP using the given username. + * User found via configurable user filter. + * @param $userName + * @return array|null + * @throws LdapException + */ public function getUserDetails($userName) { + $ldapConnection = $this->getConnection(); - if(!function_exists('ldap_connect')) { + // Find user + $userFilter = $this->buildFilter(config('services.ldap.user_filter'), ['user' => $userName]); + $baseDn = config('services.ldap.base_dn'); + $ldapSearch = ldap_search($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn']); + $users = ldap_get_entries($ldapConnection, $ldapSearch); + if ($users['count'] === 0) return null; + + $user = $users[0]; + return [ + 'uid' => $user['uid'][0], + 'name' => $user['cn'][0], + 'dn' => $user['dn'] + ]; + } + + /** + * @param Authenticatable $user + * @param string $username + * @param string $password + * @return bool + * @throws LdapException + */ + public function validateUserCredentials(Authenticatable $user, $username, $password) + { + $ldapUser = $this->getUserDetails($username); + if ($ldapUser === null) return false; + if ($ldapUser['uid'] !== $user->external_auth_id) return false; + + $ldapConnection = $this->getConnection(); + $ldapBind = @ldap_bind($ldapConnection, $ldapUser['dn'], $password); + return $ldapBind; + } + + /** + * Bind the system user to the LDAP connection using the given credentials + * otherwise anonymous access is attempted. + * @param $connection + * @throws LdapException + */ + protected function bindSystemUser($connection) + { + $ldapDn = config('services.ldap.dn'); + $ldapPass = config('services.ldap.pass'); + + $isAnonymous = ($ldapDn === false || $ldapPass === false); + if ($isAnonymous) { + $ldapBind = ldap_bind($connection); + } else { + $ldapBind = ldap_bind($connection, $ldapDn, $ldapPass); + } + + if (!$ldapBind) throw new LdapException('LDAP access failed using ' . $isAnonymous ? ' anonymous bind.' : ' given dn & pass details'); + } + + /** + * Get the connection to the LDAP server. + * Creates a new connection if one does not exist. + * @return resource + * @throws LdapException + */ + protected function getConnection() + { + if ($this->ldapConnection !== null) return $this->ldapConnection; + + // Check LDAP extension in installed + if (!function_exists('ldap_connect')) { throw new LdapException('LDAP PHP extension not installed'); } - + // Get port from server string if specified. $ldapServer = explode(':', config('services.ldap.server')); $ldapConnection = ldap_connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389); @@ -21,37 +97,24 @@ class LdapService throw new LdapException('Cannot connect to ldap server, Initial connection failed'); } - // Options - + // Set any required options ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3); // TODO - make configurable - $ldapDn = config('services.ldap.dn'); - $ldapPass = config('services.ldap.pass'); - $isAnonymous = ($ldapDn === false || $ldapPass === false); - if ($isAnonymous) { - $ldapBind = ldap_bind($ldapConnection); - } else { - $ldapBind = ldap_bind($ldapConnection, $ldapDn, $ldapPass); - } - - if (!$ldapBind) throw new LdapException('LDAP access failed using ' . $isAnonymous ? ' anonymous bind.' : ' given dn & pass details'); - - // Find user - $userFilter = $this->buildFilter(config('services.ldap.user_filter'), ['user' => $userName]); - //dd($userFilter); - $baseDn = config('services.ldap.base_dn'); - $ldapSearch = ldap_search($ldapConnection, $baseDn, $userFilter); - $users = ldap_get_entries($ldapConnection, $ldapSearch); - - dd($users); + $this->ldapConnection = $ldapConnection; + return $this->ldapConnection; } - - private function buildFilter($filterString, $attrs) + /** + * Build a filter string by injecting common variables. + * @param $filterString + * @param array $attrs + * @return string + */ + protected function buildFilter($filterString, array $attrs) { $newAttrs = []; foreach ($attrs as $key => $attrText) { - $newKey = '${'.$key.'}'; + $newKey = '${' . $key . '}'; $newAttrs[$newKey] = $attrText; } return strtr($filterString, $newAttrs); diff --git a/config/auth.php b/config/auth.php index 55d434cdf..0f2d5a69c 100644 --- a/config/auth.php +++ b/config/auth.php @@ -70,7 +70,7 @@ return [ 'providers' => [ 'users' => [ 'driver' => env('AUTH_METHOD', 'eloquent'), - 'model' => Bookstack\User::class, + 'model' => BookStack\User::class, ], // 'users' => [ diff --git a/database/migrations/2016_01_11_210908_add_external_auth_to_users.php b/database/migrations/2016_01_11_210908_add_external_auth_to_users.php new file mode 100644 index 000000000..dda8f3d74 --- /dev/null +++ b/database/migrations/2016_01_11_210908_add_external_auth_to_users.php @@ -0,0 +1,31 @@ +string('external_auth_id')->index(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('external_auth_id'); + }); + } +} diff --git a/resources/views/auth/forms/login/ldap.blade.php b/resources/views/auth/forms/login/ldap.blade.php index aa9109345..eb0a3182f 100644 --- a/resources/views/auth/forms/login/ldap.blade.php +++ b/resources/views/auth/forms/login/ldap.blade.php @@ -1,6 +1,6 @@