Passed
Pull Request — master (#235)
by
unknown
02:17
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
                $identity->getId(),
147
                $this->getAuthKey(),
148
                $duration,
149
            ],
150
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
151
        );
152
153
        $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

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

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

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