]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/Saml2Service.php
Add error messages, fix LDAP error
[bookstack] / app / Auth / Access / Saml2Service.php
1 <?php namespace BookStack\Auth\Access;
2
3 use BookStack\Auth\Access;
4 use BookStack\Auth\User;
5 use BookStack\Auth\UserRepo;
6 use BookStack\Exceptions\SamlException;
7 use Illuminate\Contracts\Auth\Authenticatable;
8
9
10 /**
11  * Class Saml2Service
12  * Handles any app-specific SAML tasks.
13  * @package BookStack\Services
14  */
15 class Saml2Service extends Access\ExternalAuthService
16 {
17     protected $config;
18     protected $userRepo;
19     protected $user;
20     protected $enabled;
21
22     /**
23      * Saml2Service constructor.
24      * @param \BookStack\Auth\UserRepo $userRepo
25      */
26     public function __construct(UserRepo $userRepo, User $user)
27     {
28         $this->config = config('services.saml');
29         $this->userRepo = $userRepo;
30         $this->user = $user;
31         $this->enabled = config('saml2_settings.enabled') === true;
32     }
33
34     /**
35      * Check if groups should be synced.
36      * @return bool
37      */
38     public function shouldSyncGroups()
39     {
40         return $this->enabled && $this->config['user_to_groups'] !== false;
41     }
42
43     /** Calculate the display name
44      *  @param array $samlAttributes
45      *  @param string $defaultValue
46      *  @return string
47      */
48     protected function getUserDisplayName(array $samlAttributes, string $defaultValue)
49     {
50         $displayNameAttr = $this->config['display_name_attribute'];
51
52         $displayName = [];
53         foreach ($displayNameAttr as $dnAttr) {
54           $dnComponent = $this->getSamlResponseAttribute($samlAttributes, $dnAttr, null);
55           if ($dnComponent !== null) {
56             $displayName[] = $dnComponent;
57           }
58         }
59
60         if (count($displayName) == 0) {
61           $displayName = $defaultValue;
62         } else {
63           $displayName = implode(' ', $displayName);
64         }
65
66         return $displayName;
67     }
68
69     protected function getUserName(array $samlAttributes, string $defaultValue)
70     {
71         $userNameAttr = $this->config['user_name_attribute'];
72
73         if ($userNameAttr === null) {
74             $userName = $defaultValue;
75         } else {
76             $userName = $this->getSamlResponseAttribute($samlAttributes, $userNameAttr, $defaultValue);
77         }
78
79         return $userName;
80     }
81
82     /**
83      * Extract the details of a user from a SAML response.
84      * @param $samlID
85      * @param $samlAttributes
86      * @return array
87      */
88     public function getUserDetails($samlID, $samlAttributes)
89     {
90         $emailAttr = $this->config['email_attribute'];
91         $userName = $this->getUserName($samlAttributes, $samlID);
92
93         return [
94             'uid'   => $userName,
95             'name'  => $this->getUserDisplayName($samlAttributes, $userName),
96             'dn'    => $samlID,
97             'email' => $this->getSamlResponseAttribute($samlAttributes, $emailAttr, null),
98         ];
99     }
100
101     /**
102      * Get the groups a user is a part of from the SAML response.
103      * @param array $samlAttributes
104      * @return array
105      */
106     public function getUserGroups($samlAttributes)
107     {
108         $groupsAttr = $this->config['group_attribute'];
109         $userGroups = $samlAttributes[$groupsAttr];
110
111         if (!is_array($userGroups)) {
112             $userGroups = [];
113         }
114
115         return $userGroups;
116     }
117
118     /**
119      *  For an array of strings, return a default for an empty array,
120      *  a string for an array with one element and the full array for
121      *  more than one element.
122      *
123      *  @param array $data
124      *  @param $defaultValue
125      *  @return string
126      */
127     protected function simplifyValue(array $data, $defaultValue) {
128         switch (count($data)) {
129             case 0:
130                 $data = $defaultValue;
131                 break;
132             case 1:
133                 $data = $data[0];
134                 break;
135         }
136         return $data;
137     }
138
139     /**
140      * Get a property from an SAML response.
141      * Handles properties potentially being an array.
142      * @param array $userDetails
143      * @param string $propertyKey
144      * @param $defaultValue
145      * @return mixed
146      */
147     protected function getSamlResponseAttribute(array $samlAttributes, string $propertyKey, $defaultValue)
148     {
149         if (isset($samlAttributes[$propertyKey])) {
150             $data = $this->simplifyValue($samlAttributes[$propertyKey], $defaultValue);
151         } else {
152             $data = $defaultValue;
153         }
154
155         return $data;
156     }
157
158     /**
159      *  Register a user that is authenticated but not
160      *  already registered.
161      *  @param array $userDetails
162      *  @return User
163      */
164     protected function registerUser($userDetails)
165     {
166         // Create an array of the user data to create a new user instance
167         $userData = [
168             'name' => $userDetails['name'],
169             'email' => $userDetails['email'],
170             'password' => str_random(30),
171             'external_auth_id' => $userDetails['uid'],
172             'email_confirmed' => true,
173         ];
174
175         $user = $this->user->forceCreate($userData);
176         $this->userRepo->attachDefaultRole($user);
177         $this->userRepo->downloadAndAssignUserAvatar($user);
178         return $user;
179     }
180
181     /**
182      * Get the user from the database for the specified details.
183      * @param array $userDetails
184      * @return User|null
185      */
186     protected function getOrRegisterUser($userDetails)
187     {
188         $isRegisterEnabled = config('services.saml.auto_register') === true;
189         $user = $this->user
190           ->where('external_auth_id', $userDetails['uid'])
191           ->first();
192
193         if ($user === null && $isRegisterEnabled) {
194             $user = $this->registerUser($userDetails);
195         }
196
197         return $user;
198     }
199
200     /**
201      *  Process the SAML response for a user. Login the user when
202      *  they exist, optionally registering them automatically.
203      *  @param string $samlID
204      *  @param array $samlAttributes
205      *  @throws SamlException
206      */
207     public function processLoginCallback($samlID, $samlAttributes)
208     {
209         $userDetails = $this->getUserDetails($samlID, $samlAttributes);
210         $isLoggedIn = auth()->check();
211
212         if ($isLoggedIn) {
213             throw new SamlException(trans('errors.saml_already_logged_in'), '/login');
214         } else {
215             $user = $this->getOrRegisterUser($userDetails);
216             if ($user === null) {
217                 throw new SamlException(trans('errors.saml_user_not_registered', ['name' => $userDetails['uid']]), '/login');
218             } else {
219                 $groups = $this->getUserGroups($samlAttributes);
220                 $this->syncWithGroups($user, $groups);
221                 auth()->login($user);
222             }
223         }
224
225         return $user;
226     }
227 }