]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/LoginService.php
Cleaned some unused elements during testing
[bookstack] / app / Auth / Access / LoginService.php
1 <?php
2
3 namespace BookStack\Auth\Access;
4
5 use BookStack\Actions\ActivityType;
6 use BookStack\Auth\Access\Mfa\MfaSession;
7 use BookStack\Auth\User;
8 use BookStack\Exceptions\StoppedAuthenticationException;
9 use BookStack\Facades\Activity;
10 use BookStack\Facades\Theme;
11 use BookStack\Theming\ThemeEvents;
12 use Exception;
13
14 class LoginService
15 {
16
17     protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
18
19     protected $mfaSession;
20     protected $emailConfirmationService;
21
22     public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
23     {
24         $this->mfaSession = $mfaSession;
25         $this->emailConfirmationService = $emailConfirmationService;
26     }
27
28     /**
29      * Log the given user into the system.
30      * Will start a login of the given user but will prevent if there's
31      * a reason to (MFA or Unconfirmed Email).
32      * Returns a boolean to indicate the current login result.
33      * @throws StoppedAuthenticationException
34      */
35     public function login(User $user, string $method, bool $remember = false): void
36     {
37         if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {
38             $this->setLastLoginAttemptedForUser($user, $method, $remember);
39             throw new StoppedAuthenticationException($user, $this);
40         }
41
42         $this->clearLastLoginAttempted();
43         auth()->login($user, $remember);
44         Activity::add(ActivityType::AUTH_LOGIN, "{$method}; {$user->logDescriptor()}");
45         Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
46
47         // Authenticate on all session guards if a likely admin
48         if ($user->can('users-manage') && $user->can('user-roles-manage')) {
49             $guards = ['standard', 'ldap', 'saml2'];
50             foreach ($guards as $guard) {
51                 auth($guard)->login($user);
52             }
53         }
54     }
55
56     /**
57      * Reattempt a system login after a previous stopped attempt.
58      * @throws Exception
59      */
60     public function reattemptLoginFor(User $user)
61     {
62         if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {
63             throw new Exception('Login reattempt user does align with current session state');
64         }
65
66         $lastLoginDetails = $this->getLastLoginAttemptDetails();
67         $this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false);
68     }
69
70     /**
71      * Get the last user that was attempted to be logged in.
72      * Only exists if the last login attempt had correct credentials
73      * but had been prevented by a secondary factor.
74      */
75     public function getLastLoginAttemptUser(): ?User
76     {
77         $id = $this->getLastLoginAttemptDetails()['user_id'];
78         return User::query()->where('id', '=', $id)->first();
79     }
80
81     /**
82      * Get the details of the last login attempt.
83      * Checks upon a ttl of about 1 hour since that last attempted login.
84      * @return array{user_id: ?string, method: ?string, remember: bool}
85      */
86     protected function getLastLoginAttemptDetails(): array
87     {
88         $value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
89         if (!$value) {
90             return ['user_id' => null, 'method' => null];
91         }
92
93         [$id, $method, $remember, $time] = explode(':', $value);
94         $hourAgo = time() - (60*60);
95         if ($time < $hourAgo) {
96             $this->clearLastLoginAttempted();
97             return ['user_id' => null, 'method' => null];
98         }
99
100         return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];
101     }
102
103     /**
104      * Set the last login attempted user.
105      * Must be only used when credentials are correct and a login could be
106      * achieved but a secondary factor has stopped the login.
107      */
108     protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember)
109     {
110         session()->put(
111             self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
112             implode(':', [$user->id, $method, $remember, time()])
113         );
114     }
115
116     /**
117      * Clear the last login attempted session value.
118      */
119     protected function clearLastLoginAttempted(): void
120     {
121         session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
122     }
123
124     /**
125      * Check if MFA verification is needed.
126      */
127     public function needsMfaVerification(User $user): bool
128     {
129         return !$this->mfaSession->isVerifiedForUser($user) && $this->mfaSession->isRequiredForUser($user);
130     }
131
132     /**
133      * Check if the given user is awaiting email confirmation.
134      */
135     public function awaitingEmailConfirmation(User $user): bool
136     {
137         return $this->emailConfirmationService->confirmationRequired() && !$user->email_confirmed;
138     }
139
140     /**
141      * Attempt the login of a user using the given credentials.
142      * Meant to mirror Laravel's default guard 'attempt' method
143      * but in a manner that always routes through our login system.
144      * May interrupt the flow if extra authentication requirements are imposed.
145      *
146      * @throws StoppedAuthenticationException
147      */
148     public function attempt(array $credentials, string $method, bool $remember = false): bool
149     {
150         $result = auth()->attempt($credentials, $remember);
151         if ($result) {
152             $user = auth()->user();
153             auth()->logout();
154             $this->login($user, $method, $remember);
155         }
156
157         return $result;
158     }
159
160 }