]> BookStack Code Mirror - bookstack/blob - app/Services/SocialAuthService.php
Fix ajax tag suggestion for subdir installs
[bookstack] / app / Services / SocialAuthService.php
1 <?php namespace BookStack\Services;
2
3 use Laravel\Socialite\Contracts\Factory as Socialite;
4 use BookStack\Exceptions\SocialDriverNotConfigured;
5 use BookStack\Exceptions\SocialSignInException;
6 use BookStack\Exceptions\UserRegistrationException;
7 use BookStack\Repos\UserRepo;
8 use BookStack\SocialAccount;
9
10 class SocialAuthService
11 {
12
13     protected $userRepo;
14     protected $socialite;
15     protected $socialAccount;
16
17     protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure'];
18
19     /**
20      * SocialAuthService constructor.
21      * @param UserRepo      $userRepo
22      * @param Socialite     $socialite
23      * @param SocialAccount $socialAccount
24      */
25     public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
26     {
27         $this->userRepo = $userRepo;
28         $this->socialite = $socialite;
29         $this->socialAccount = $socialAccount;
30     }
31
32
33     /**
34      * Start the social login path.
35      * @param string $socialDriver
36      * @return \Symfony\Component\HttpFoundation\RedirectResponse
37      * @throws SocialDriverNotConfigured
38      */
39     public function startLogIn($socialDriver)
40     {
41         $driver = $this->validateDriver($socialDriver);
42         return $this->socialite->driver($driver)->redirect();
43     }
44
45     /**
46      * Start the social registration process
47      * @param string $socialDriver
48      * @return \Symfony\Component\HttpFoundation\RedirectResponse
49      * @throws SocialDriverNotConfigured
50      */
51     public function startRegister($socialDriver)
52     {
53         $driver = $this->validateDriver($socialDriver);
54         return $this->socialite->driver($driver)->redirect();
55     }
56
57     /**
58      * Handle the social registration process on callback.
59      * @param $socialDriver
60      * @return \Laravel\Socialite\Contracts\User
61      * @throws SocialDriverNotConfigured
62      * @throws UserRegistrationException
63      */
64     public function handleRegistrationCallback($socialDriver)
65     {
66         $driver = $this->validateDriver($socialDriver);
67
68         // Get user details from social driver
69         $socialUser = $this->socialite->driver($driver)->user();
70
71         // Check social account has not already been used
72         if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
73             throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
74         }
75
76         if ($this->userRepo->getByEmail($socialUser->getEmail())) {
77             $email = $socialUser->getEmail();
78             throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
79         }
80
81         return $socialUser;
82     }
83
84     /**
85      * Handle the login process on a oAuth callback.
86      * @param $socialDriver
87      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
88      * @throws SocialDriverNotConfigured
89      * @throws SocialSignInException
90      */
91     public function handleLoginCallback($socialDriver)
92     {
93         $driver = $this->validateDriver($socialDriver);
94
95         // Get user details from social driver
96         $socialUser = $this->socialite->driver($driver)->user();
97         $socialId = $socialUser->getId();
98
99         // Get any attached social accounts or users
100         $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
101         $isLoggedIn = auth()->check();
102         $currentUser = user();
103
104         // When a user is not logged in and a matching SocialAccount exists,
105         // Simply log the user into the application.
106         if (!$isLoggedIn && $socialAccount !== null) {
107             auth()->login($socialAccount->user);
108             return redirect()->intended('/');
109         }
110
111         // When a user is logged in but the social account does not exist,
112         // Create the social account and attach it to the user & redirect to the profile page.
113         if ($isLoggedIn && $socialAccount === null) {
114             $this->fillSocialAccount($socialDriver, $socialUser);
115             $currentUser->socialAccounts()->save($this->socialAccount);
116             session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
117             return redirect($currentUser->getEditUrl());
118         }
119
120         // When a user is logged in and the social account exists and is already linked to the current user.
121         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
122             session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
123             return redirect($currentUser->getEditUrl());
124         }
125
126         // When a user is logged in, A social account exists but the users do not match.
127         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
128             session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
129             return redirect($currentUser->getEditUrl());
130         }
131
132         // Otherwise let the user know this social account is not used by anyone.
133         $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
134         if (setting('registration-enabled')) {
135             $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
136         }
137         
138         throw new SocialSignInException($message . '.', '/login');
139     }
140
141     /**
142      * Ensure the social driver is correct and supported.
143      *
144      * @param $socialDriver
145      * @return string
146      * @throws SocialDriverNotConfigured
147      */
148     private function validateDriver($socialDriver)
149     {
150         $driver = trim(strtolower($socialDriver));
151
152         if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
153         if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
154
155         return $driver;
156     }
157
158     /**
159      * Check a social driver has been configured correctly.
160      * @param $driver
161      * @return bool
162      */
163     private function checkDriverConfigured($driver)
164     {
165         $lowerName = strtolower($driver);
166         $configPrefix = 'services.' . $lowerName . '.';
167         $config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
168         return !in_array(false, $config) && !in_array(null, $config);
169     }
170
171     /**
172      * Gets the names of the active social drivers.
173      * @return array
174      */
175     public function getActiveDrivers()
176     {
177         $activeDrivers = [];
178         foreach ($this->validSocialDrivers as $driverKey) {
179             if ($this->checkDriverConfigured($driverKey)) {
180                 $activeDrivers[$driverKey] = $this->getDriverName($driverKey);
181             }
182         }
183         return $activeDrivers;
184     }
185
186     /**
187      * Get the presentational name for a driver.
188      * @param $driver
189      * @return mixed
190      */
191     public function getDriverName($driver)
192     {
193         return config('services.' . strtolower($driver) . '.name');
194     }
195
196     /**
197      * @param string                            $socialDriver
198      * @param \Laravel\Socialite\Contracts\User $socialUser
199      * @return SocialAccount
200      */
201     public function fillSocialAccount($socialDriver, $socialUser)
202     {
203         $this->socialAccount->fill([
204             'driver'    => $socialDriver,
205             'driver_id' => $socialUser->getId(),
206             'avatar'    => $socialUser->getAvatar()
207         ]);
208         return $this->socialAccount;
209     }
210
211     /**
212      * Detach a social account from a user.
213      * @param $socialDriver
214      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
215      */
216     public function detachSocialAccount($socialDriver)
217     {
218         user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
219         session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
220         return redirect(user()->getEditUrl());
221     }
222
223 }