Passed
Pull Request — master (#235)
by
unknown
02:00
created

User::getAuthKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Yiisoft\Yii\Web\User;
4
5
use Nyholm\Psr7\Response;
6
use Psr\EventDispatcher\EventDispatcherInterface;
7
use Yiisoft\Access\AccessCheckerInterface;
8
use Yiisoft\Auth\IdentityInterface;
9
use Yiisoft\Auth\IdentityRepositoryInterface;
10
use Yiisoft\Yii\Web\Cookie;
11
use Yiisoft\Yii\Web\Session\SessionInterface;
12
use Yiisoft\Yii\Web\User\Event\AfterLogin;
13
use Yiisoft\Yii\Web\User\Event\AfterLogout;
14
use Yiisoft\Yii\Web\User\Event\BeforeLogin;
15
use Yiisoft\Yii\Web\User\Event\BeforeLogout;
16
17
class User
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 IdentityRepositoryInterface $identityRepository;
24
    private EventDispatcherInterface $eventDispatcher;
25
26
    private ?AccessCheckerInterface $accessChecker = null;
27
    private ?IdentityInterface $identity = null;
28
    private ?SessionInterface $session = null;
29
30
    public function __construct(
31
        IdentityRepositoryInterface $identityRepository,
32
        EventDispatcherInterface $eventDispatcher
33
    ) {
34
        $this->identityRepository = $identityRepository;
35
        $this->eventDispatcher = $eventDispatcher;
36
    }
37
38
    /**
39
     * @var int|null the number of seconds in which the user will be logged out automatically if he
40
     * remains inactive. If this property is not set, the user will be logged out after
41
     * the current session expires (c.f. [[Session::timeout]]).
42
     */
43
    public ?int $authTimeout = null;
44
45
    /**
46
     * @var int|null the number of seconds in which the user will be logged out automatically
47
     * regardless of activity.
48
     * Note that this will not work if [[enableAutoLogin]] is `true`.
49
     */
50
    public ?int $absoluteAuthTimeout = null;
51
52
    /**
53
     * @var array MIME types for which this component should redirect to the [[loginUrl]].
54
     */
55
    public array $acceptableRedirectTypes = ['text/html', 'application/xhtml+xml'];
56
57
    /**
58
     * Set session to persist authentication status across multiple requests.
59
     * If not set, authentication has to be performed on each request, which is often the case
60
     * for stateless application such as RESTful API.
61
     *
62
     * @param SessionInterface $session
63
     */
64
    public function setSession(SessionInterface $session): void
65
    {
66
        $this->session = $session;
67
    }
68
69
    public function setAccessChecker(AccessCheckerInterface $accessChecker): void
70
    {
71
        $this->accessChecker = $accessChecker;
72
    }
73
74
    /**
75
     * Returns the identity object associated with the currently logged-in user.
76
     * When [[enableSession]] is true, this method may attempt to read the user's authentication data
77
     * stored in session and reconstruct the corresponding identity object, if it has not done so before.
78
     * @param bool $autoRenew whether to automatically renew authentication status if it has not been done so before.
79
     * This is only useful when [[enableSession]] is true.
80
     * @return IdentityInterface the identity object associated with the currently logged-in user.
81
     * @throws \Throwable
82
     * @see logout()
83
     * @see login()
84
     */
85
    public function getIdentity($autoRenew = true): IdentityInterface
86
    {
87
        if ($this->identity !== null) {
88
            return $this->identity;
89
        }
90
        if ($this->session === null || !$autoRenew) {
91
            return new GuestIdentity();
92
        }
93
        try {
94
            $this->renewAuthStatus();
95
        } catch (\Throwable $e) {
96
            $this->identity = null;
97
            throw $e;
98
        }
99
        return $this->identity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->identity could return the type null which is incompatible with the type-hinted return Yiisoft\Auth\IdentityInterface. Consider adding an additional type-check to rule them out.
Loading history...
100
    }
101
102
    /**
103
     * Sets the user identity object.
104
     *
105
     * Note that this method does not deal with session or cookie. You should usually use [[switchIdentity()]]
106
     * to change the identity of the current user.
107
     *
108
     * @param IdentityInterface|null $identity the identity object associated with the currently logged user.
109
     * Use {{@see GuestIdentity}} to indicate that the current user is a guest.
110
     */
111
    public function setIdentity(IdentityInterface $identity): void
112
    {
113
        $this->identity = $identity;
114
    }
115
116
    /**
117
     * Return the auth key value
118
     *
119
     * @return string Auth key value
120
     */
121
    public function getAuthKey(): string
122
    {
123
        return 'ABCD1234';
124
    }
125
126
    /**
127
     * Validate auth key
128
     *
129
     * @param String $authKey Auth key to validate
130
     * @return bool True if is valid
131
     */
132
    public function validateAuthKey($authKey): bool
133
    {
134
        return $authKey === 'ABCD1234';
135
    }
136
137
    /**
138
     * Sends an identity cookie.
139
     *
140
     * @param IdentityInterface $identity
141
     * @param int $duration number of seconds that the user can remain in logged-in status.
142
     */
143
    protected function sendIdentityCookie(IdentityInterface $identity, int $duration): void
144
    {
145
        $data = json_encode(
146
            [
147
                $identity->getId(),
148
                $this->getAuthKey(),
149
                $duration,
150
            ],
151
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
152
        );
153
154
        $cookieIdentity = (new Cookie('remember', $data))->expireAt(time() + $duration);
0 ignored issues
show
Bug introduced by
time() + $duration of type integer is incompatible with the type DateTimeInterface expected by parameter $dateTime of Yiisoft\Yii\Web\Cookie::expireAt(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
        $cookieIdentity = (new Cookie('remember', $data))->expireAt(/** @scrutinizer ignore-type */ time() + $duration);
Loading history...
155
        $response = new Response();
156
        $cookieIdentity->addToResponse($response);
157
    }
158
159
    /**
160
     * Logs in a user.
161
     *
162
     * After logging in a user:
163
     * - the user's identity information is obtainable from the [[identity]] property
164
     *
165
     * If [[enableSession]] is `true`:
166
     * - the identity information will be stored in session and be available in the next requests
167
     * - in case of `$duration == 0`: as long as the session remains active or till the user closes the browser
168
     * - in case of `$duration > 0`: as long as the session remains active or as long as the cookie
169
     *   remains valid by it's `$duration` in seconds when [[enableAutoLogin]] is set `true`.
170
     *
171
     * If [[enableSession]] is `false`:
172
     * - the `$duration` parameter will be ignored
173
     *
174
     * @param IdentityInterface $identity the user identity (which should already be authenticated)
175
     * @param int $duration number of seconds that the user can remain in logged-in status, defaults to `0`
176
     * @return bool whether the user is logged in
177
     */
178
    public function login(IdentityInterface $identity, int $duration = 0): bool
179
    {
180
        if ($this->beforeLogin($identity, $duration)) {
181
            $this->switchIdentity($identity);
182
            $this->afterLogin($identity, $duration);
183
            $this->sendIdentityCookie($identity, $duration);
184
        }
185
        return !$this->isGuest();
186
    }
187
188
    /**
189
     * Logs in a user by the given access token.
190
     * This method will first authenticate the user by calling [[IdentityInterface::findIdentityByAccessToken()]]
191
     * with the provided access token. If successful, it will call [[login()]] to log in the authenticated user.
192
     * If authentication fails or [[login()]] is unsuccessful, it will return null.
193
     * @param string $token the access token
194
     * @param string $type the type of the token. The value of this parameter depends on the implementation.
195
     * For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be `yii\filters\auth\HttpBearerAuth`.
196
     * @return IdentityInterface|null the identity associated with the given access token. Null is returned if
197
     * the access token is invalid or [[login()]] is unsuccessful.
198
     */
199
    public function loginByAccessToken(string $token, string $type = null): ?IdentityInterface
200
    {
201
        $identity = $this->identityRepository->findIdentityByToken($token, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type null; however, parameter $type of Yiisoft\Auth\IdentityRep...::findIdentityByToken() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
        $identity = $this->identityRepository->findIdentityByToken($token, /** @scrutinizer ignore-type */ $type);
Loading history...
202
        if ($identity && $this->login($identity)) {
203
            return $identity;
204
        }
205
        return null;
206
    }
207
208
    /**
209
     * Logs out the current user.
210
     * This will remove authentication-related session data.
211
     * If `$destroySession` is true, all session data will be removed.
212
     * @param bool $destroySession whether to destroy the whole session. Defaults to true.
213
     * This parameter is ignored if [[enableSession]] is false.
214
     * @return bool whether the user is logged out
215
     * @throws \Throwable
216
     */
217
    public function logout($destroySession = true): bool
218
    {
219
        $identity = $this->getIdentity();
220
        if ($this->isGuest()) {
221
            return false;
222
        }
223
        if ($this->beforeLogout($identity)) {
224
            $this->switchIdentity(new GuestIdentity());
225
            if ($destroySession && $this->session) {
226
                $this->session->destroy();
227
            }
228
            $this->afterLogout($identity);
229
        }
230
        return $this->isGuest();
231
    }
232
233
    /**
234
     * Returns a value indicating whether the user is a guest (not authenticated).
235
     * @return bool whether the current user is a guest.
236
     * @see getIdentity()
237
     */
238
    public function isGuest(): bool
239
    {
240
        return $this->getIdentity() instanceof GuestIdentity;
241
    }
242
243
    /**
244
     * Returns a value that uniquely represents the user.
245
     * @return string the unique identifier for the user. If `null`, it means the user is a guest.
246
     * @throws \Throwable
247
     * @see getIdentity()
248
     */
249
    public function getId(): ?string
250
    {
251
        return $this->getIdentity()->getId();
252
    }
253
254
    /**
255
     * This method is called before logging in a user.
256
     * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event.
257
     * If you override this method, make sure you call the parent implementation
258
     * so that the event is triggered.
259
     * @param IdentityInterface $identity the user identity information
260
     * @param int $duration number of seconds that the user can remain in logged-in status.
261
     * If 0, it means login till the user closes the browser or the session is manually destroyed.
262
     * @return bool whether the user should continue to be logged in
263
     */
264
    protected function beforeLogin(IdentityInterface $identity, int $duration): bool
265
    {
266
        $event = new BeforeLogin($identity, $duration);
267
        $this->eventDispatcher->dispatch($event);
268
        return $event->isValid();
269
    }
270
271
    /**
272
     * This method is called after the user is successfully logged in.
273
     * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event.
274
     * If you override this method, make sure you call the parent implementation
275
     * so that the event is triggered.
276
     * @param IdentityInterface $identity the user identity information
277
     * @param int $duration number of seconds that the user can remain in logged-in status.
278
     * If 0, it means login till the user closes the browser or the session is manually destroyed.
279
     */
280
    protected function afterLogin(IdentityInterface $identity, int $duration): void
281
    {
282
        $this->eventDispatcher->dispatch(new AfterLogin($identity, $duration));
283
    }
284
285
    /**
286
     * This method is invoked when calling [[logout()]] to log out a user.
287
     * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event.
288
     * If you override this method, make sure you call the parent implementation
289
     * so that the event is triggered.
290
     * @param IdentityInterface $identity the user identity information
291
     * @return bool whether the user should continue to be logged out
292
     */
293
    protected function beforeLogout(IdentityInterface $identity): bool
294
    {
295
        $event = new BeforeLogout($identity);
296
        $this->eventDispatcher->dispatch($event);
297
        return $event->isValid();
298
    }
299
300
    /**
301
     * This method is invoked right after a user is logged out via [[logout()]].
302
     * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event.
303
     * If you override this method, make sure you call the parent implementation
304
     * so that the event is triggered.
305
     * @param IdentityInterface $identity the user identity information
306
     */
307
    protected function afterLogout(IdentityInterface $identity): void
308
    {
309
        $this->eventDispatcher->dispatch(new AfterLogout($identity));
310
    }
311
312
    /**
313
     * Switches to a new identity for the current user.
314
     *
315
     * When [[enableSession]] is true, this method may use session and/or cookie to store the user identity information,
316
     * according to the value of `$duration`. Please refer to [[login()]] for more details.
317
     *
318
     * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]]
319
     * when the current user needs to be associated with the corresponding identity information.
320
     *
321
     * @param IdentityInterface $identity the identity information to be associated with the current user.
322
     * In order to indicate that the user is guest, use {{@see GuestIdentity}}.
323
     */
324
    public function switchIdentity(IdentityInterface $identity): void
325
    {
326
        $this->setIdentity($identity);
327
        if ($this->session === null) {
328
            return;
329
        }
330
331
        $this->session->regenerateID();
332
333
        $this->session->remove(self::SESSION_AUTH_ID);
334
        $this->session->remove(self::SESSION_AUTH_EXPIRE);
335
336
        if ($identity->getId() === null) {
337
            return;
338
        }
339
        $this->session->set(self::SESSION_AUTH_ID, $identity->getId());
340
        if ($this->authTimeout !== null) {
341
            $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
342
        }
343
        if ($this->absoluteAuthTimeout !== null) {
344
            $this->session->set(self::SESSION_AUTH_ABSOLUTE_EXPIRE, time() + $this->absoluteAuthTimeout);
345
        }
346
    }
347
348
    /**
349
     * Updates the authentication status using the information from session and cookie.
350
     *
351
     * This method will try to determine the user identity using a session variable.
352
     *
353
     * If [[authTimeout]] is set, this method will refresh the timer.
354
     *
355
     * If the user identity cannot be determined by session, this method will try to [[loginByCookie()|login by cookie]]
356
     * if [[enableAutoLogin]] is true.
357
     * @throws \Throwable
358
     */
359
    protected function renewAuthStatus(): void
360
    {
361
        $id = $this->session->get(self::SESSION_AUTH_ID);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

361
        /** @scrutinizer ignore-call */ 
362
        $id = $this->session->get(self::SESSION_AUTH_ID);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
362
363
        $identity = null;
364
        if ($id !== null) {
365
            $identity = $this->identityRepository->findIdentity($id);
366
        }
367
        if ($identity === null) {
368
            $identity = new GuestIdentity();
369
        }
370
        $this->setIdentity($identity);
371
372
        if (!($identity instanceof GuestIdentity) && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
373
            $expire = $this->authTimeout !== null ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE) : null;
374
            $expireAbsolute = $this->absoluteAuthTimeout !== null ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE) : null;
375
            if (($expire !== null && $expire < time()) || ($expireAbsolute !== null && $expireAbsolute < time())) {
376
                $this->logout(false);
377
            } elseif ($this->authTimeout !== null) {
378
                $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
379
            }
380
        }
381
    }
382
383
    /**
384
     * Checks if the user can perform the operation as specified by the given permission.
385
     *
386
     * Note that you must provide access checker via {{@see User::setAccessChecker()}} in order to use this method.
387
     * Otherwise it will always return false.
388
     *
389
     * @param string $permissionName the name of the permission (e.g. "edit post") that needs access check.
390
     * @param array $params name-value pairs that would be passed to the rules associated
391
     * with the roles and permissions assigned to the user.
392
     * @return bool whether the user can perform the operation as specified by the given permission.
393
     * @throws \Throwable
394
     */
395
    public function can(string $permissionName, array $params = []): bool
396
    {
397
        if ($this->accessChecker === null) {
398
            return false;
399
        }
400
401
        return $this->accessChecker->userHasPermission($this->getId(), $permissionName, $params);
402
    }
403
}
404