]> BookStack Code Mirror - bookstack/blob - app/Services/SocialAuthService.php
Fixes #58
[bookstack] / app / Services / SocialAuthService.php
1 <?php namespace BookStack\Services;
2
3 use GuzzleHttp\Exception\ClientException;
4 use Laravel\Socialite\Contracts\Factory as Socialite;
5 use BookStack\Exceptions\SocialDriverNotConfigured;
6 use BookStack\Exceptions\SocialSignInException;
7 use BookStack\Exceptions\UserRegistrationException;
8 use BookStack\Http\Controllers\Auth\AuthController;
9 use BookStack\Repos\UserRepo;
10 use BookStack\SocialAccount;
11 use BookStack\User;
12
13 class SocialAuthService
14 {
15
16     protected $userRepo;
17     protected $socialite;
18     protected $socialAccount;
19
20     protected $validSocialDrivers = ['google', 'github'];
21
22     /**
23      * SocialAuthService constructor.
24      * @param UserRepo      $userRepo
25      * @param Socialite     $socialite
26      * @param SocialAccount $socialAccount
27      */
28     public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
29     {
30         $this->userRepo = $userRepo;
31         $this->socialite = $socialite;
32         $this->socialAccount = $socialAccount;
33     }
34
35
36     /**
37      * Start the social login path.
38      * @param string $socialDriver
39      * @return \Symfony\Component\HttpFoundation\RedirectResponse
40      * @throws SocialDriverNotConfigured
41      */
42     public function startLogIn($socialDriver)
43     {
44         $driver = $this->validateDriver($socialDriver);
45         return $this->socialite->driver($driver)->redirect();
46     }
47
48     /**
49      * Start the social registration process
50      * @param string $socialDriver
51      * @return \Symfony\Component\HttpFoundation\RedirectResponse
52      * @throws SocialDriverNotConfigured
53      */
54     public function startRegister($socialDriver)
55     {
56         $driver = $this->validateDriver($socialDriver);
57         return $this->socialite->driver($driver)->redirect();
58     }
59
60     /**
61      * Handle the social registration process on callback.
62      * @param $socialDriver
63      * @return \Laravel\Socialite\Contracts\User
64      * @throws SocialDriverNotConfigured
65      * @throws UserRegistrationException
66      */
67     public function handleRegistrationCallback($socialDriver)
68     {
69         $driver = $this->validateDriver($socialDriver);
70
71         // Get user details from social driver
72         $socialUser = $this->socialite->driver($driver)->user();
73
74         // Check social account has not already been used
75         if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
76             throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
77         }
78
79         if ($this->userRepo->getByEmail($socialUser->getEmail())) {
80             $email = $socialUser->getEmail();
81             throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
82         }
83
84         return $socialUser;
85     }
86
87     /**
88      * Handle the login process on a oAuth callback.
89      * @param $socialDriver
90      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
91      * @throws SocialDriverNotConfigured
92      * @throws SocialSignInException
93      */
94     public function handleLoginCallback($socialDriver)
95     {
96         $driver = $this->validateDriver($socialDriver);
97
98         // Get user details from social driver
99         $socialUser = $this->socialite->driver($driver)->user();
100         $socialId = $socialUser->getId();
101
102         // Get any attached social accounts or users
103         $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
104         $user = $this->userRepo->getByEmail($socialUser->getEmail());
105         $isLoggedIn = auth()->check();
106         $currentUser = auth()->user();
107
108         // When a user is not logged in and a matching SocialAccount exists,
109         // Simply log the user into the application.
110         if (!$isLoggedIn && $socialAccount !== null) {
111             return $this->logUserIn($socialAccount->user);
112         }
113
114         // When a user is logged in but the social account does not exist,
115         // Create the social account and attach it to the user & redirect to the profile page.
116         if ($isLoggedIn && $socialAccount === null) {
117             $this->fillSocialAccount($socialDriver, $socialUser);
118             $currentUser->socialAccounts()->save($this->socialAccount);
119             \Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
120             return redirect($currentUser->getEditUrl());
121         }
122
123         // When a user is logged in and the social account exists and is already linked to the current user.
124         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
125             \Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
126             return redirect($currentUser->getEditUrl());
127         }
128
129         // When a user is logged in, A social account exists but the users do not match.
130         // Change the user that the social account is assigned to.
131         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
132             \Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
133             return redirect($currentUser->getEditUrl());
134         }
135
136         // Otherwise let the user know this social account is not used by anyone.
137         $message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
138         if (\Setting::get('registration-enabled')) {
139             $message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
140         }
141         throw new SocialSignInException($message . '.', '/login');
142     }
143
144
145     private function logUserIn($user)
146     {
147         auth()->login($user);
148         return redirect('/');
149     }
150
151     /**
152      * Ensure the social driver is correct and supported.
153      *
154      * @param $socialDriver
155      * @return string
156      * @throws SocialDriverNotConfigured
157      */
158     private function validateDriver($socialDriver)
159     {
160         $driver = trim(strtolower($socialDriver));
161
162         if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
163         if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
164
165         return $driver;
166     }
167
168     /**
169      * Check a social driver has been configured correctly.
170      * @param $driver
171      * @return bool
172      */
173     private function checkDriverConfigured($driver)
174     {
175         $lowerName = strtolower($driver);
176         $configPrefix = 'services.' . $lowerName . '.';
177         $config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
178         return !in_array(false, $config) && !in_array(null, $config);
179     }
180
181     /**
182      * Gets the names of the active social drivers.
183      * @return array
184      */
185     public function getActiveDrivers()
186     {
187         $activeDrivers = [];
188         foreach ($this->validSocialDrivers as $driverName) {
189             if ($this->checkDriverConfigured($driverName)) {
190                 $activeDrivers[$driverName] = true;
191             }
192         }
193         return $activeDrivers;
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         session();
219         auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
220         \Session::flash('success', $socialDriver . ' account successfully detached');
221         return redirect(auth()->user()->getEditUrl());
222     }
223
224 }