]> BookStack Code Mirror - bookstack/blob - app/Services/SocialAuthService.php
set uploaded files public visibliity (relevant for S3 storage)
[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'];
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('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
74         }
75
76         if ($this->userRepo->getByEmail($socialUser->getEmail())) {
77             $email = $socialUser->getEmail();
78             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');
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         $user = $this->userRepo->getByEmail($socialUser->getEmail());
102         $isLoggedIn = auth()->check();
103         $currentUser = auth()->user();
104
105         // When a user is not logged in and a matching SocialAccount exists,
106         // Simply log the user into the application.
107         if (!$isLoggedIn && $socialAccount !== null) {
108             return $this->logUserIn($socialAccount->user);
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', title_case($socialDriver) . ' account was successfully attached to your profile.');
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', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
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         // Change the user that the social account is assigned to.
128         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
129             \Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
130             return redirect($currentUser->getEditUrl());
131         }
132
133         // Otherwise let the user know this social account is not used by anyone.
134         $message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
135         if (setting('registration-enabled')) {
136             $message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
137         }
138         throw new SocialSignInException($message . '.', '/login');
139     }
140
141
142     private function logUserIn($user)
143     {
144         auth()->login($user);
145         return redirect('/');
146     }
147
148     /**
149      * Ensure the social driver is correct and supported.
150      *
151      * @param $socialDriver
152      * @return string
153      * @throws SocialDriverNotConfigured
154      */
155     private function validateDriver($socialDriver)
156     {
157         $driver = trim(strtolower($socialDriver));
158
159         if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
160         if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
161
162         return $driver;
163     }
164
165     /**
166      * Check a social driver has been configured correctly.
167      * @param $driver
168      * @return bool
169      */
170     private function checkDriverConfigured($driver)
171     {
172         $lowerName = strtolower($driver);
173         $configPrefix = 'services.' . $lowerName . '.';
174         $config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
175         return !in_array(false, $config) && !in_array(null, $config);
176     }
177
178     /**
179      * Gets the names of the active social drivers.
180      * @return array
181      */
182     public function getActiveDrivers()
183     {
184         $activeDrivers = [];
185         foreach ($this->validSocialDrivers as $driverName) {
186             if ($this->checkDriverConfigured($driverName)) {
187                 $activeDrivers[$driverName] = true;
188             }
189         }
190         return $activeDrivers;
191     }
192
193     /**
194      * @param string                            $socialDriver
195      * @param \Laravel\Socialite\Contracts\User $socialUser
196      * @return SocialAccount
197      */
198     public function fillSocialAccount($socialDriver, $socialUser)
199     {
200         $this->socialAccount->fill([
201             'driver'    => $socialDriver,
202             'driver_id' => $socialUser->getId(),
203             'avatar'    => $socialUser->getAvatar()
204         ]);
205         return $this->socialAccount;
206     }
207
208     /**
209      * Detach a social account from a user.
210      * @param $socialDriver
211      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
212      */
213     public function detachSocialAccount($socialDriver)
214     {
215         session();
216         auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
217         \Session::flash('success', $socialDriver . ' account successfully detached');
218         return redirect(auth()->user()->getEditUrl());
219     }
220
221 }