Dan Brown

Got LDAP auth working to a functional state

...@@ -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
...@@ -55,7 +63,7 @@ class LdapUserProvider implements UserProvider ...@@ -55,7 +63,7 @@ class LdapUserProvider implements UserProvider
55 * Retrieve a user by their unique identifier and "remember me" token. 63 * Retrieve a user by their unique identifier and "remember me" token.
56 * 64 *
57 * @param mixed $identifier 65 * @param mixed $identifier
58 - * @param string $token 66 + * @param string $token
59 * @return \Illuminate\Contracts\Auth\Authenticatable|null 67 * @return \Illuminate\Contracts\Auth\Authenticatable|null
60 */ 68 */
61 public function retrieveByToken($identifier, $token) 69 public function retrieveByToken($identifier, $token)
...@@ -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]);
14 - } 25 + $baseDn = config('services.ldap.base_dn');
15 - 26 + $ldapSearch = ldap_search($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn']);
16 - 27 + $users = ldap_get_entries($ldapConnection, $ldapSearch);
17 - $ldapServer = explode(':', config('services.ldap.server')); 28 + if ($users['count'] === 0) return null;
18 - $ldapConnection = ldap_connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389); 29 +
19 - 30 + $user = $users[0];
20 - if ($ldapConnection === false) { 31 + return [
21 - throw new LdapException('Cannot connect to ldap server, Initial connection failed'); 32 + 'uid' => $user['uid'][0],
22 - } 33 + 'name' => $user['cn'][0],
34 + 'dn' => $user['dn']
35 + ];
36 + }
23 37
24 - // Options 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;
25 50
26 - ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3); // TODO - make configurable 51 + $ldapConnection = $this->getConnection();
52 + $ldapBind = @ldap_bind($ldapConnection, $ldapUser['dn'], $password);
53 + return $ldapBind;
54 + }
27 55
56 + /**
57 + * Bind the system user to the LDAP connection using the given credentials
58 + * otherwise anonymous access is attempted.
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
47 - } 88 + if (!function_exists('ldap_connect')) {
89 + throw new LdapException('LDAP PHP extension not installed');
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">
......