Test Failed
Pull Request — master (#24)
by Sergei
85:01 queued 83:05
created

SessionAuthenticator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 3
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\User;
6
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Throwable;
9
use Yiisoft\Auth\IdentityInterface;
10
use Yiisoft\Auth\IdentityRepositoryInterface;
11
use Yiisoft\Session\SessionInterface;
12
use Yiisoft\User\Event\AfterLogin;
13
use Yiisoft\User\Event\AfterLogout;
14
use Yiisoft\User\Event\BeforeLogin;
15
use Yiisoft\User\Event\BeforeLogout;
16
17
final class SessionAuthenticator implements AuthenticatorInterface
18
{
19
    private const SESSION_AUTH_ID = '__auth_id';
20
    private const SESSION_AUTH_EXPIRE = '__auth_expire';
21
    private const SESSION_AUTH_ABSOLUTE_EXPIRE = '__auth_absolute_expire';
22
23
    private ?IdentityInterface $identity = null;
24
25
    /**
26
     * @var int|null the number of seconds in which the user will be logged out automatically in case of
27
     * remaining inactive. If this property is not set, the user will be logged out after
28
     * the current session expires.
29
     */
30
    private ?int $authTimeout = null;
31
32
    /**
33
     * @var int|null the number of seconds in which the user will be logged out automatically
34
     * regardless of activity.
35
     */
36
    private ?int $absoluteAuthTimeout = null;
37
38
    private IdentityRepositoryInterface $identityRepository;
39
    private EventDispatcherInterface $eventDispatcher;
40
    private ?SessionInterface $session;
41
42
    /**
43
     * @param SessionInterface|null $session session to persist authentication status across multiple requests.
44
     * If not set, authentication has to be performed on each request, which is often the case for stateless
45
     * application such as RESTful API.
46
     */
47
    public function __construct(
48
        IdentityRepositoryInterface $identityRepository,
49
        EventDispatcherInterface $eventDispatcher,
50
        SessionInterface $session = null
51
    ) {
52
        $this->identityRepository = $identityRepository;
53
        $this->eventDispatcher = $eventDispatcher;
54
        $this->session = $session;
55
    }
56
57
    public function setAuthTimeout(int $timeout = null): self
58
    {
59
        $this->authTimeout = $timeout;
60
        return $this;
61
    }
62
63
    public function setAbsoluteAuthTimeout(int $timeout = null): self
64
    {
65
        $this->absoluteAuthTimeout = $timeout;
66
        return $this;
67
    }
68
69
    /**
70
     * Returns the identity object associated with the currently logged-in user.
71
     * This method read the user's authentication data
72
     * stored in session and reconstruct the corresponding identity object, if it has not done so before.
73
     *
74
     * @param bool $autoRenew whether to automatically renew authentication status if it has not been done so before.
75
     *
76
     * @throws Throwable
77
     *
78
     * @return IdentityInterface the identity object associated with the currently logged-in user.
79
     *
80
     * @see logout()
81
     * @see login()
82
     */
83
    public function getIdentity(bool $autoRenew = true): IdentityInterface
84
    {
85
        if ($this->identity !== null) {
86
            return $this->identity;
87
        }
88
        if ($this->session === null || !$autoRenew) {
89
            return new GuestIdentity();
90
        }
91
        try {
92
            $this->renewAuthStatus();
93
        } catch (Throwable $e) {
94
            $this->identity = null;
95
            throw $e;
96
        }
97
98
        /** @psalm-suppress TypeDoesNotContainType */
99
        return $this->identity ?? new GuestIdentity();
100
    }
101
102
    /**
103
     * Switches to a new identity for the current user.
104
     *
105
     * This method use session to store the user identity information.
106
     * Please refer to {@see login()} for more details.
107
     *
108
     * This method is mainly called by {@see login()} and {@see logout()}
109
     * when the current user needs to be associated with the corresponding identity information.
110
     *
111
     * @param IdentityInterface $identity the identity information to be associated with the current user.
112
     * In order to indicate that the user is guest, use {{@see GuestIdentity}}.
113
     */
114
    private function switchIdentity(IdentityInterface $identity): void
115
    {
116
        $this->identity = $identity;
117
        if ($this->session === null) {
118
            return;
119
        }
120
121
        $this->session->regenerateID();
122
123
        $this->session->remove(self::SESSION_AUTH_ID);
124
        $this->session->remove(self::SESSION_AUTH_EXPIRE);
125
126
        if ($identity->getId() === null) {
127
            return;
128
        }
129
        $this->session->set(self::SESSION_AUTH_ID, $identity->getId());
130
        if ($this->authTimeout !== null) {
131
            $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
132
        }
133
        if ($this->absoluteAuthTimeout !== null) {
134
            $this->session->set(self::SESSION_AUTH_ABSOLUTE_EXPIRE, time() + $this->absoluteAuthTimeout);
135
        }
136
    }
137
138
    /**
139
     * Returns a value indicating whether the user is a guest (not authenticated).
140
     *
141
     * @return bool whether the current user is a guest.
142
     *
143
     * @see getIdentity()
144
     */
145
    private function isGuest(): bool
146
    {
147
        return $this->getIdentity() instanceof GuestIdentity;
148
    }
149
150
    /**
151
     * Logs in a user.
152
     *
153
     * After logging in a user:
154
     * - the user's identity information is obtainable from the {@see getIdentity()}
155
     * - the identity information will be stored in session and be available in the next requests as long as the session
156
     *   remains active or till the user closes the browser. Some browsers, such as Chrome, are keeping session when
157
     *   browser is re-opened.
158
     *
159
     * @param IdentityInterface $identity the user identity (which should already be authenticated)
160
     *
161
     * @return bool whether the user is logged in
162
     */
163
    public function login(IdentityInterface $identity): bool
164
    {
165
        if ($this->beforeLogin($identity)) {
166
            $this->switchIdentity($identity);
167
            $this->afterLogin($identity);
168
        }
169
        return !$this->isGuest();
170
    }
171
172
    /**
173
     * This method is called before logging in a user.
174
     * The default implementation will trigger the {@see BeforeLogin} event.
175
     * If you override this method, make sure you call the parent implementation
176
     * so that the event is triggered.
177
     *
178
     * @param IdentityInterface $identity the user identity information
179
     *
180
     * @return bool whether the user should continue to be logged in
181
     */
182
    private function beforeLogin(IdentityInterface $identity): bool
183
    {
184
        $event = new BeforeLogin($identity);
185
        $this->eventDispatcher->dispatch($event);
186
        return $event->isValid();
187
    }
188
189
    /**
190
     * This method is called after the user is successfully logged in.
191
     *
192
     * @param IdentityInterface $identity the user identity information
193
     */
194
    private function afterLogin(IdentityInterface $identity): void
195
    {
196
        $this->eventDispatcher->dispatch(new AfterLogin($identity));
197
    }
198
199
    /**
200
     * Logs out the current user.
201
     * This will remove authentication-related session data.
202
     * If `$destroySession` is true, all session data will be removed.
203
     *
204
     * @param bool $destroySession whether to destroy the whole session. Defaults to true.
205
     *
206
     * @throws Throwable
207
     *
208
     * @return bool whether the user is logged out
209
     */
210
    public function logout(bool $destroySession = true): bool
211
    {
212
        $identity = $this->getIdentity();
213
        if ($this->isGuest()) {
214
            return false;
215
        }
216
        if ($this->beforeLogout($identity)) {
217
            $this->switchIdentity(new GuestIdentity());
218
            if ($destroySession && $this->session) {
219
                $this->session->destroy();
220
            }
221
222
            $this->afterLogout($identity);
223
        }
224
        return $this->isGuest();
225
    }
226
227
    /**
228
     * This method is invoked when calling {@see logout()} to log out a user.
229
     *
230
     * @param IdentityInterface $identity the user identity information
231
     *
232
     * @return bool whether the user should continue to be logged out
233
     */
234
    private function beforeLogout(IdentityInterface $identity): bool
235
    {
236
        $event = new BeforeLogout($identity);
237
        $this->eventDispatcher->dispatch($event);
238
        return $event->isValid();
239
    }
240
241
    /**
242
     * This method is invoked right after a user is logged out via {@see logout()}.
243
     *
244
     * @param IdentityInterface $identity the user identity information
245
     */
246
    private function afterLogout(IdentityInterface $identity): void
247
    {
248
        $this->eventDispatcher->dispatch(new AfterLogout($identity));
249
    }
250
251
    /**
252
     * Updates the authentication status using the information from session.
253
     *
254
     * This method will try to determine the user identity using a session variable.
255
     *
256
     * If {@see authTimeout} is set, this method will refresh the timer.
257
     *
258
     * @throws Throwable
259
     */
260
    private function renewAuthStatus(): void
261
    {
262
        if ($this->session === null) {
263
            $this->identity = new GuestIdentity();
264
            return;
265
        }
266
267
        /** @var mixed $id */
268
        $id = $this->session->get(self::SESSION_AUTH_ID);
269
270
        $identity = null;
271
        if ($id !== null) {
272
            $identity = $this->identityRepository->findIdentity((string)$id);
273
        }
274
        if ($identity === null) {
275
            $identity = new GuestIdentity();
276
        }
277
        $this->identity = $identity;
278
279
        if (
280
            !($identity instanceof GuestIdentity) &&
281
            ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)
282
        ) {
283
            /** @var mixed $expire */
284
            $expire = $this->authTimeout !== null ? $this->session->get(self::SESSION_AUTH_EXPIRE) : null;
285
            if ($expire !== null) {
286
                $expire = (int)$expire;
287
            }
288
289
            /** @var mixed $expireAbsolute */
290
            $expireAbsolute = $this->absoluteAuthTimeout !== null
291
                ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE)
292
                : null;
293
            if ($expireAbsolute !== null) {
294
                $expireAbsolute = (int)$expire;
295
            }
296
297
            if (($expire !== null && $expire < time()) || ($expireAbsolute !== null && $expireAbsolute < time())) {
298
                $this->logout(false);
299
            } elseif ($this->authTimeout !== null) {
300
                $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
301
            }
302
        }
303
    }
304
}
305