]> BookStack Code Mirror - bookstack/blob - app/Access/SocialAuthService.php
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / app / Access / SocialAuthService.php
1 <?php
2
3 namespace BookStack\Access;
4
5 use BookStack\Exceptions\SocialDriverNotConfigured;
6 use BookStack\Exceptions\SocialSignInAccountNotUsed;
7 use BookStack\Exceptions\UserRegistrationException;
8 use BookStack\Users\Models\User;
9 use Illuminate\Support\Str;
10 use Laravel\Socialite\Contracts\Factory as Socialite;
11 use Laravel\Socialite\Contracts\Provider;
12 use Laravel\Socialite\Contracts\User as SocialUser;
13 use Laravel\Socialite\Two\GoogleProvider;
14 use Symfony\Component\HttpFoundation\RedirectResponse;
15
16 class SocialAuthService
17 {
18     public function __construct(
19         protected Socialite $socialite,
20         protected LoginService $loginService,
21         protected SocialDriverManager $driverManager,
22     ) {
23     }
24
25     /**
26      * Start the social login path.
27      *
28      * @throws SocialDriverNotConfigured
29      */
30     public function startLogIn(string $socialDriver): RedirectResponse
31     {
32         $socialDriver = trim(strtolower($socialDriver));
33         $this->driverManager->ensureDriverActive($socialDriver);
34
35         return $this->getDriverForRedirect($socialDriver)->redirect();
36     }
37
38     /**
39      * Start the social registration process.
40      *
41      * @throws SocialDriverNotConfigured
42      */
43     public function startRegister(string $socialDriver): RedirectResponse
44     {
45         $socialDriver = trim(strtolower($socialDriver));
46         $this->driverManager->ensureDriverActive($socialDriver);
47
48         return $this->getDriverForRedirect($socialDriver)->redirect();
49     }
50
51     /**
52      * Handle the social registration process on callback.
53      *
54      * @throws UserRegistrationException
55      */
56     public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
57     {
58         // Check social account has not already been used
59         if (SocialAccount::query()->where('driver_id', '=', $socialUser->getId())->exists()) {
60             throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount' => $socialDriver]), '/login');
61         }
62
63         if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
64             $email = $socialUser->getEmail();
65
66             throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
67         }
68
69         return $socialUser;
70     }
71
72     /**
73      * Get the social user details via the social driver.
74      *
75      * @throws SocialDriverNotConfigured
76      */
77     public function getSocialUser(string $socialDriver): SocialUser
78     {
79         $socialDriver = trim(strtolower($socialDriver));
80         $this->driverManager->ensureDriverActive($socialDriver);
81
82         return $this->socialite->driver($socialDriver)->user();
83     }
84
85     /**
86      * Handle the login process on a oAuth callback.
87      *
88      * @throws SocialSignInAccountNotUsed
89      */
90     public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
91     {
92         $socialDriver = trim(strtolower($socialDriver));
93         $socialId = $socialUser->getId();
94
95         // Get any attached social accounts or users
96         $socialAccount = SocialAccount::query()->where('driver_id', '=', $socialId)->first();
97         $isLoggedIn = auth()->check();
98         $currentUser = user();
99         $titleCaseDriver = Str::title($socialDriver);
100
101         // When a user is not logged in and a matching SocialAccount exists,
102         // Simply log the user into the application.
103         if (!$isLoggedIn && $socialAccount !== null) {
104             $this->loginService->login($socialAccount->user, $socialDriver);
105
106             return redirect()->intended('/');
107         }
108
109         // When a user is logged in but the social account does not exist,
110         // Create the social account and attach it to the user & redirect to the profile page.
111         if ($isLoggedIn && $socialAccount === null) {
112             $account = $this->newSocialAccount($socialDriver, $socialUser);
113             $currentUser->socialAccounts()->save($account);
114             session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
115
116             return redirect('/my-account/auth#social_accounts');
117         }
118
119         // When a user is logged in and the social account exists and is already linked to the current user.
120         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
121             session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
122
123             return redirect('/my-account/auth#social_accounts');
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' => $titleCaseDriver]));
129
130             return redirect('/my-account/auth#social_accounts');
131         }
132
133         // Otherwise let the user know this social account is not used by anyone.
134         $message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
135         if (setting('registration-enabled') && config('auth.method') !== 'ldap' && config('auth.method') !== 'saml2') {
136             $message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
137         }
138
139         throw new SocialSignInAccountNotUsed($message, '/login');
140     }
141
142     /**
143      * Get the social driver manager used by this service.
144      */
145     public function drivers(): SocialDriverManager
146     {
147         return $this->driverManager;
148     }
149
150     /**
151      * Fill and return a SocialAccount from the given driver name and SocialUser.
152      */
153     public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
154     {
155         return new SocialAccount([
156             'driver'    => $socialDriver,
157             'driver_id' => $socialUser->getId(),
158             'avatar'    => $socialUser->getAvatar(),
159         ]);
160     }
161
162     /**
163      * Detach a social account from a user.
164      */
165     public function detachSocialAccount(string $socialDriver): void
166     {
167         user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
168     }
169
170     /**
171      * Provide redirect options per service for the Laravel Socialite driver.
172      */
173     protected function getDriverForRedirect(string $driverName): Provider
174     {
175         $driver = $this->socialite->driver($driverName);
176
177         if ($driver instanceof GoogleProvider && config('services.google.select_account')) {
178             $driver->with(['prompt' => 'select_account']);
179         }
180
181         $this->driverManager->getConfigureForRedirectCallback($driverName)($driver);
182
183         return $driver;
184     }
185 }