Showing
10 changed files
with
169 additions
and
39 deletions
| ... | @@ -2,6 +2,7 @@ | ... | @@ -2,6 +2,7 @@ |
| 2 | 2 | ||
| 3 | namespace BookStack\Http\Controllers\Auth; | 3 | namespace BookStack\Http\Controllers\Auth; |
| 4 | 4 | ||
| 5 | +use Illuminate\Contracts\Auth\Authenticatable; | ||
| 5 | use Illuminate\Http\Request; | 6 | use Illuminate\Http\Request; |
| 6 | use BookStack\Exceptions\SocialSignInException; | 7 | use BookStack\Exceptions\SocialSignInException; |
| 7 | use BookStack\Exceptions\UserRegistrationException; | 8 | use BookStack\Exceptions\UserRegistrationException; |
| ... | @@ -31,6 +32,8 @@ class AuthController extends Controller | ... | @@ -31,6 +32,8 @@ class AuthController extends Controller |
| 31 | 32 | ||
| 32 | protected $redirectPath = '/'; | 33 | protected $redirectPath = '/'; |
| 33 | protected $redirectAfterLogout = '/login'; | 34 | protected $redirectAfterLogout = '/login'; |
| 35 | + protected $username = 'email'; | ||
| 36 | + | ||
| 34 | 37 | ||
| 35 | protected $socialAuthService; | 38 | protected $socialAuthService; |
| 36 | protected $emailConfirmationService; | 39 | protected $emailConfirmationService; |
| ... | @@ -48,6 +51,7 @@ class AuthController extends Controller | ... | @@ -48,6 +51,7 @@ class AuthController extends Controller |
| 48 | $this->socialAuthService = $socialAuthService; | 51 | $this->socialAuthService = $socialAuthService; |
| 49 | $this->emailConfirmationService = $emailConfirmationService; | 52 | $this->emailConfirmationService = $emailConfirmationService; |
| 50 | $this->userRepo = $userRepo; | 53 | $this->userRepo = $userRepo; |
| 54 | + $this->username = config('auth.method') === 'standard' ? 'email' : 'username'; | ||
| 51 | parent::__construct(); | 55 | parent::__construct(); |
| 52 | } | 56 | } |
| 53 | 57 | ||
| ... | @@ -104,6 +108,24 @@ class AuthController extends Controller | ... | @@ -104,6 +108,24 @@ class AuthController extends Controller |
| 104 | return $this->registerUser($userData); | 108 | return $this->registerUser($userData); |
| 105 | } | 109 | } |
| 106 | 110 | ||
| 111 | + | ||
| 112 | + /** | ||
| 113 | + * Overrides the action when a user is authenticated. | ||
| 114 | + * If the user authenticated but does not exist in the user table we create them. | ||
| 115 | + * @param Request $request | ||
| 116 | + * @param Authenticatable $user | ||
| 117 | + * @return \Illuminate\Http\RedirectResponse | ||
| 118 | + */ | ||
| 119 | + protected function authenticated(Request $request, Authenticatable $user) | ||
| 120 | + { | ||
| 121 | + if(!$user->exists) { | ||
| 122 | + $user->save(); | ||
| 123 | + $this->userRepo->attachDefaultRole($user); | ||
| 124 | + auth()->login($user); | ||
| 125 | + } | ||
| 126 | + return redirect()->intended($this->redirectPath()); | ||
| 127 | + } | ||
| 128 | + | ||
| 107 | /** | 129 | /** |
| 108 | * Register a new user after a registration callback. | 130 | * Register a new user after a registration callback. |
| 109 | * @param $socialDriver | 131 | * @param $socialDriver |
| ... | @@ -232,7 +254,7 @@ class AuthController extends Controller | ... | @@ -232,7 +254,7 @@ class AuthController extends Controller |
| 232 | public function getLogin() | 254 | public function getLogin() |
| 233 | { | 255 | { |
| 234 | $socialDrivers = $this->socialAuthService->getActiveDrivers(); | 256 | $socialDrivers = $this->socialAuthService->getActiveDrivers(); |
| 235 | - $authMethod = 'standard'; // TODO - rewrite to use config. | 257 | + $authMethod = config('auth.method'); |
| 236 | return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]); | 258 | return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]); |
| 237 | } | 259 | } |
| 238 | 260 | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | Route::get('/test', function() { | 3 | Route::get('/test', function() { |
| 4 | // TODO - remove this | 4 | // TODO - remove this |
| 5 | $service = new \BookStack\Services\LdapService(); | 5 | $service = new \BookStack\Services\LdapService(); |
| 6 | - $service->getUserDetails('ssmith'); | 6 | + dd($service->getUserDetails('ksmith')); |
| 7 | }); | 7 | }); |
| 8 | 8 | ||
| 9 | // Authenticated routes... | 9 | // Authenticated routes... | ... | ... |
| ... | @@ -25,7 +25,7 @@ class AuthServiceProvider extends ServiceProvider | ... | @@ -25,7 +25,7 @@ class AuthServiceProvider extends ServiceProvider |
| 25 | public function register() | 25 | public function register() |
| 26 | { | 26 | { |
| 27 | Auth::provider('ldap', function($app, array $config) { | 27 | Auth::provider('ldap', function($app, array $config) { |
| 28 | - return new LdapUserProvider($config['model']); | 28 | + return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']); |
| 29 | }); | 29 | }); |
| 30 | } | 30 | } |
| 31 | } | 31 | } | ... | ... |
| ... | @@ -3,6 +3,8 @@ | ... | @@ -3,6 +3,8 @@ |
| 3 | namespace BookStack\Providers; | 3 | namespace BookStack\Providers; |
| 4 | 4 | ||
| 5 | 5 | ||
| 6 | +use BookStack\Role; | ||
| 7 | +use BookStack\Services\LdapService; | ||
| 6 | use BookStack\User; | 8 | use BookStack\User; |
| 7 | use Illuminate\Contracts\Auth\Authenticatable; | 9 | use Illuminate\Contracts\Auth\Authenticatable; |
| 8 | use Illuminate\Contracts\Auth\UserProvider; | 10 | use Illuminate\Contracts\Auth\UserProvider; |
| ... | @@ -17,14 +19,21 @@ class LdapUserProvider implements UserProvider | ... | @@ -17,14 +19,21 @@ class LdapUserProvider implements UserProvider |
| 17 | */ | 19 | */ |
| 18 | protected $model; | 20 | protected $model; |
| 19 | 21 | ||
| 22 | + /** | ||
| 23 | + * @var LdapService | ||
| 24 | + */ | ||
| 25 | + protected $ldapService; | ||
| 26 | + | ||
| 20 | 27 | ||
| 21 | /** | 28 | /** |
| 22 | * LdapUserProvider constructor. | 29 | * LdapUserProvider constructor. |
| 23 | * @param $model | 30 | * @param $model |
| 31 | + * @param LdapService $ldapService | ||
| 24 | */ | 32 | */ |
| 25 | - public function __construct($model) | 33 | + public function __construct($model, LdapService $ldapService) |
| 26 | { | 34 | { |
| 27 | $this->model = $model; | 35 | $this->model = $model; |
| 36 | + $this->ldapService = $ldapService; | ||
| 28 | } | 37 | } |
| 29 | 38 | ||
| 30 | /** | 39 | /** |
| ... | @@ -34,8 +43,7 @@ class LdapUserProvider implements UserProvider | ... | @@ -34,8 +43,7 @@ class LdapUserProvider implements UserProvider |
| 34 | */ | 43 | */ |
| 35 | public function createModel() | 44 | public function createModel() |
| 36 | { | 45 | { |
| 37 | - $class = '\\'.ltrim($this->model, '\\'); | 46 | + $class = '\\' . ltrim($this->model, '\\'); |
| 38 | - | ||
| 39 | return new $class; | 47 | return new $class; |
| 40 | } | 48 | } |
| 41 | 49 | ||
| ... | @@ -91,16 +99,21 @@ class LdapUserProvider implements UserProvider | ... | @@ -91,16 +99,21 @@ class LdapUserProvider implements UserProvider |
| 91 | */ | 99 | */ |
| 92 | public function retrieveByCredentials(array $credentials) | 100 | public function retrieveByCredentials(array $credentials) |
| 93 | { | 101 | { |
| 94 | - // TODO: Implement retrieveByCredentials() method. | ||
| 95 | - | ||
| 96 | // Get user via LDAP | 102 | // Get user via LDAP |
| 103 | + $userDetails = $this->ldapService->getUserDetails($credentials['username']); | ||
| 104 | + if ($userDetails === null) return null; | ||
| 97 | 105 | ||
| 98 | // Search current user base by looking up a uid | 106 | // Search current user base by looking up a uid |
| 107 | + $model = $this->createModel(); | ||
| 108 | + $currentUser = $model->newQuery() | ||
| 109 | + ->where('external_auth_id', $userDetails['uid']) | ||
| 110 | + ->first(); | ||
| 99 | 111 | ||
| 100 | - // If not exists create a new user instance with attached role | 112 | + if ($currentUser !== null) return $currentUser; |
| 101 | - // but do not store it in the database yet | ||
| 102 | 113 | ||
| 103 | - // | 114 | + $model->name = $userDetails['name']; |
| 115 | + $model->external_auth_id = $userDetails['uid']; | ||
| 116 | + return $model; | ||
| 104 | } | 117 | } |
| 105 | 118 | ||
| 106 | /** | 119 | /** |
| ... | @@ -112,6 +125,6 @@ class LdapUserProvider implements UserProvider | ... | @@ -112,6 +125,6 @@ class LdapUserProvider implements UserProvider |
| 112 | */ | 125 | */ |
| 113 | public function validateCredentials(Authenticatable $user, array $credentials) | 126 | public function validateCredentials(Authenticatable $user, array $credentials) |
| 114 | { | 127 | { |
| 115 | - // TODO: Implement validateCredentials() method. | 128 | + return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']); |
| 116 | } | 129 | } |
| 117 | } | 130 | } | ... | ... |
| ... | @@ -3,6 +3,7 @@ | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 | ||
| 4 | use BookStack\Role; | 4 | use BookStack\Role; |
| 5 | use BookStack\User; | 5 | use BookStack\User; |
| 6 | +use Setting; | ||
| 6 | 7 | ||
| 7 | class UserRepo | 8 | class UserRepo |
| 8 | { | 9 | { |
| ... | @@ -56,7 +57,7 @@ class UserRepo | ... | @@ -56,7 +57,7 @@ class UserRepo |
| 56 | */ | 57 | */ |
| 57 | public function attachDefaultRole($user) | 58 | public function attachDefaultRole($user) |
| 58 | { | 59 | { |
| 59 | - $roleId = \Setting::get('registration-role'); | 60 | + $roleId = Setting::get('registration-role'); |
| 60 | if ($roleId === false) $roleId = $this->role->getDefault()->id; | 61 | if ($roleId === false) $roleId = $this->role->getDefault()->id; |
| 61 | $user->attachRoleId($roleId); | 62 | $user->attachRoleId($roleId); |
| 62 | } | 63 | } | ... | ... |
| ... | @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model; | ... | @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model; |
| 7 | class Role extends Model | 7 | class Role extends Model |
| 8 | { | 8 | { |
| 9 | /** | 9 | /** |
| 10 | - * Sets the default role name for newly registed users. | 10 | + * Sets the default role name for newly registered users. |
| 11 | * @var string | 11 | * @var string |
| 12 | */ | 12 | */ |
| 13 | protected static $default = 'viewer'; | 13 | protected static $default = 'viewer'; | ... | ... |
| ... | @@ -2,56 +2,119 @@ | ... | @@ -2,56 +2,119 @@ |
| 2 | 2 | ||
| 3 | 3 | ||
| 4 | use BookStack\Exceptions\LdapException; | 4 | use BookStack\Exceptions\LdapException; |
| 5 | +use Illuminate\Contracts\Auth\Authenticatable; | ||
| 5 | 6 | ||
| 6 | class LdapService | 7 | class LdapService |
| 7 | { | 8 | { |
| 8 | 9 | ||
| 10 | + protected $ldapConnection; | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * Get the details of a user from LDAP using the given username. | ||
| 14 | + * User found via configurable user filter. | ||
| 15 | + * @param $userName | ||
| 16 | + * @return array|null | ||
| 17 | + * @throws LdapException | ||
| 18 | + */ | ||
| 9 | public function getUserDetails($userName) | 19 | public function getUserDetails($userName) |
| 10 | { | 20 | { |
| 21 | + $ldapConnection = $this->getConnection(); | ||
| 11 | 22 | ||
| 12 | - if(!function_exists('ldap_connect')) { | 23 | + // Find user |
| 13 | - throw new LdapException('LDAP PHP extension not installed'); | 24 | + $userFilter = $this->buildFilter(config('services.ldap.user_filter'), ['user' => $userName]); |
| 25 | + $baseDn = config('services.ldap.base_dn'); | ||
| 26 | + $ldapSearch = ldap_search($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn']); | ||
| 27 | + $users = ldap_get_entries($ldapConnection, $ldapSearch); | ||
| 28 | + if ($users['count'] === 0) return null; | ||
| 29 | + | ||
| 30 | + $user = $users[0]; | ||
| 31 | + return [ | ||
| 32 | + 'uid' => $user['uid'][0], | ||
| 33 | + 'name' => $user['cn'][0], | ||
| 34 | + 'dn' => $user['dn'] | ||
| 35 | + ]; | ||
| 14 | } | 36 | } |
| 15 | 37 | ||
| 38 | + /** | ||
| 39 | + * @param Authenticatable $user | ||
| 40 | + * @param string $username | ||
| 41 | + * @param string $password | ||
| 42 | + * @return bool | ||
| 43 | + * @throws LdapException | ||
| 44 | + */ | ||
| 45 | + public function validateUserCredentials(Authenticatable $user, $username, $password) | ||
| 46 | + { | ||
| 47 | + $ldapUser = $this->getUserDetails($username); | ||
| 48 | + if ($ldapUser === null) return false; | ||
| 49 | + if ($ldapUser['uid'] !== $user->external_auth_id) return false; | ||
| 16 | 50 | ||
| 17 | - $ldapServer = explode(':', config('services.ldap.server')); | 51 | + $ldapConnection = $this->getConnection(); |
| 18 | - $ldapConnection = ldap_connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389); | 52 | + $ldapBind = @ldap_bind($ldapConnection, $ldapUser['dn'], $password); |
| 19 | - | 53 | + return $ldapBind; |
| 20 | - if ($ldapConnection === false) { | ||
| 21 | - throw new LdapException('Cannot connect to ldap server, Initial connection failed'); | ||
| 22 | } | 54 | } |
| 23 | 55 | ||
| 24 | - // Options | 56 | + /** |
| 25 | - | 57 | + * Bind the system user to the LDAP connection using the given credentials |
| 26 | - ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3); // TODO - make configurable | 58 | + * otherwise anonymous access is attempted. |
| 27 | - | 59 | + * @param $connection |
| 60 | + * @throws LdapException | ||
| 61 | + */ | ||
| 62 | + protected function bindSystemUser($connection) | ||
| 63 | + { | ||
| 28 | $ldapDn = config('services.ldap.dn'); | 64 | $ldapDn = config('services.ldap.dn'); |
| 29 | $ldapPass = config('services.ldap.pass'); | 65 | $ldapPass = config('services.ldap.pass'); |
| 66 | + | ||
| 30 | $isAnonymous = ($ldapDn === false || $ldapPass === false); | 67 | $isAnonymous = ($ldapDn === false || $ldapPass === false); |
| 31 | if ($isAnonymous) { | 68 | if ($isAnonymous) { |
| 32 | - $ldapBind = ldap_bind($ldapConnection); | 69 | + $ldapBind = ldap_bind($connection); |
| 33 | } else { | 70 | } else { |
| 34 | - $ldapBind = ldap_bind($ldapConnection, $ldapDn, $ldapPass); | 71 | + $ldapBind = ldap_bind($connection, $ldapDn, $ldapPass); |
| 35 | } | 72 | } |
| 36 | 73 | ||
| 37 | if (!$ldapBind) throw new LdapException('LDAP access failed using ' . $isAnonymous ? ' anonymous bind.' : ' given dn & pass details'); | 74 | if (!$ldapBind) throw new LdapException('LDAP access failed using ' . $isAnonymous ? ' anonymous bind.' : ' given dn & pass details'); |
| 75 | + } | ||
| 38 | 76 | ||
| 39 | - // Find user | 77 | + /** |
| 40 | - $userFilter = $this->buildFilter(config('services.ldap.user_filter'), ['user' => $userName]); | 78 | + * Get the connection to the LDAP server. |
| 41 | - //dd($userFilter); | 79 | + * Creates a new connection if one does not exist. |
| 42 | - $baseDn = config('services.ldap.base_dn'); | 80 | + * @return resource |
| 43 | - $ldapSearch = ldap_search($ldapConnection, $baseDn, $userFilter); | 81 | + * @throws LdapException |
| 44 | - $users = ldap_get_entries($ldapConnection, $ldapSearch); | 82 | + */ |
| 83 | + protected function getConnection() | ||
| 84 | + { | ||
| 85 | + if ($this->ldapConnection !== null) return $this->ldapConnection; | ||
| 45 | 86 | ||
| 46 | - dd($users); | 87 | + // Check LDAP extension in installed |
| 88 | + if (!function_exists('ldap_connect')) { | ||
| 89 | + throw new LdapException('LDAP PHP extension not installed'); | ||
| 47 | } | 90 | } |
| 48 | 91 | ||
| 92 | + // Get port from server string if specified. | ||
| 93 | + $ldapServer = explode(':', config('services.ldap.server')); | ||
| 94 | + $ldapConnection = ldap_connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389); | ||
| 95 | + | ||
| 96 | + if ($ldapConnection === false) { | ||
| 97 | + throw new LdapException('Cannot connect to ldap server, Initial connection failed'); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + // Set any required options | ||
| 101 | + ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3); // TODO - make configurable | ||
| 102 | + | ||
| 103 | + $this->ldapConnection = $ldapConnection; | ||
| 104 | + return $this->ldapConnection; | ||
| 105 | + } | ||
| 49 | 106 | ||
| 50 | - private function buildFilter($filterString, $attrs) | 107 | + /** |
| 108 | + * Build a filter string by injecting common variables. | ||
| 109 | + * @param $filterString | ||
| 110 | + * @param array $attrs | ||
| 111 | + * @return string | ||
| 112 | + */ | ||
| 113 | + protected function buildFilter($filterString, array $attrs) | ||
| 51 | { | 114 | { |
| 52 | $newAttrs = []; | 115 | $newAttrs = []; |
| 53 | foreach ($attrs as $key => $attrText) { | 116 | foreach ($attrs as $key => $attrText) { |
| 54 | - $newKey = '${'.$key.'}'; | 117 | + $newKey = '${' . $key . '}'; |
| 55 | $newAttrs[$newKey] = $attrText; | 118 | $newAttrs[$newKey] = $attrText; |
| 56 | } | 119 | } |
| 57 | return strtr($filterString, $newAttrs); | 120 | return strtr($filterString, $newAttrs); | ... | ... |
| ... | @@ -70,7 +70,7 @@ return [ | ... | @@ -70,7 +70,7 @@ return [ |
| 70 | 'providers' => [ | 70 | 'providers' => [ |
| 71 | 'users' => [ | 71 | 'users' => [ |
| 72 | 'driver' => env('AUTH_METHOD', 'eloquent'), | 72 | 'driver' => env('AUTH_METHOD', 'eloquent'), |
| 73 | - 'model' => Bookstack\User::class, | 73 | + 'model' => BookStack\User::class, |
| 74 | ], | 74 | ], |
| 75 | 75 | ||
| 76 | // 'users' => [ | 76 | // 'users' => [ | ... | ... |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +use Illuminate\Database\Schema\Blueprint; | ||
| 4 | +use Illuminate\Database\Migrations\Migration; | ||
| 5 | + | ||
| 6 | +class AddExternalAuthToUsers extends Migration | ||
| 7 | +{ | ||
| 8 | + /** | ||
| 9 | + * Run the migrations. | ||
| 10 | + * | ||
| 11 | + * @return void | ||
| 12 | + */ | ||
| 13 | + public function up() | ||
| 14 | + { | ||
| 15 | + Schema::table('users', function (Blueprint $table) { | ||
| 16 | + $table->string('external_auth_id')->index(); | ||
| 17 | + }); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * Reverse the migrations. | ||
| 22 | + * | ||
| 23 | + * @return void | ||
| 24 | + */ | ||
| 25 | + public function down() | ||
| 26 | + { | ||
| 27 | + Schema::table('users', function (Blueprint $table) { | ||
| 28 | + $table->dropColumn('external_auth_id'); | ||
| 29 | + }); | ||
| 30 | + } | ||
| 31 | +} |
| 1 | <div class="form-group"> | 1 | <div class="form-group"> |
| 2 | - <label for="email">Username</label> | 2 | + <label for="username">Username</label> |
| 3 | - @include('form/text', ['name' => 'email', 'tabindex' => 1]) | 3 | + @include('form/text', ['name' => 'username', 'tabindex' => 1]) |
| 4 | </div> | 4 | </div> |
| 5 | 5 | ||
| 6 | <div class="form-group"> | 6 | <div class="form-group"> | ... | ... |
-
Please register or sign in to post a comment