Passed
Push — master ( df3c7c...3f99e9 )
by Melech
05:17
created

ORMAdapter::authenticate()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 13
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Auth\Adapters;
15
16
use Exception;
17
use Valkyrja\Auth\Adapter as Contract;
18
use Valkyrja\Auth\Exceptions\InvalidAuthenticationException;
19
use Valkyrja\Auth\Exceptions\InvalidRegistrationException;
20
use Valkyrja\Auth\LockableUser;
21
use Valkyrja\Auth\User;
22
use Valkyrja\Crypt\Crypt;
23
use Valkyrja\Crypt\Exceptions\CryptException;
24
use Valkyrja\ORM\ORM;
25
use Valkyrja\ORM\Repository;
26
use Valkyrja\Support\Type\Str;
27
28
use const PASSWORD_DEFAULT;
29
30
/**
31
 * Class Adapter.
32
 *
33
 * @author Melech Mizrachi
34
 */
35
class ORMAdapter implements Contract
36
{
37
    /**
38
     * The crypt.
39
     *
40
     * @var Crypt
41
     */
42
    protected Crypt $crypt;
43
44
    /**
45
     * The orm
46
     *
47
     * @var ORM
48
     */
49
    protected ORM $orm;
50
51
    /**
52
     * Adapter constructor.
53
     *
54
     * @param Crypt $crypt The crypt
55
     * @param ORM   $orm   The orm
56
     */
57
    public function __construct(Crypt $crypt, ORM $orm)
58
    {
59
        $this->crypt = $crypt;
60
        $this->orm   = $orm;
61
    }
62
63
    /**
64
     * Attempt to authenticate a user.
65
     *
66
     * @param User $user
67
     *
68
     * @return bool
69
     */
70
    public function authenticate(User $user): bool
71
    {
72
        $dbUser = $this->getUserViaLoginFields($user);
73
74
        // If there is a user and the password matches
75
        if ($dbUser && $this->isPassword($dbUser, $user->__get($user::getPasswordField()))) {
76
            // Update the user model with all the properties from the database
77
            $user->__setProperties($dbUser->__storable());
78
79
            return true;
80
        }
81
82
        return false;
83
    }
84
85
    /**
86
     * Get the user token.
87
     *
88
     * @param User $user
89
     *
90
     * @throws CryptException
91
     *
92
     * @return string
93
     */
94
    public function getToken(User $user): string
95
    {
96
        // Get the password field
97
        $passwordField = $user::getPasswordField();
98
99
        $user->__expose($passwordField);
100
101
        $token = $this->crypt->encryptArray($user->__tokenized());
102
103
        $user->__unexpose($passwordField);
104
105
        return $token;
106
    }
107
108
    /**
109
     * Determine if a token is valid.
110
     *
111
     * @param string $token
112
     *
113
     * @return bool
114
     */
115
    public function isValidToken(string $token): bool
116
    {
117
        return $this->crypt->isValidEncryptedMessage($token);
118
    }
119
120
    /**
121
     * Get a user from token.
122
     *
123
     * @param string $user
124
     * @param string $token
125
     *
126
     * @return User|null
127
     */
128
    public function getUserFromToken(string $user, string $token): ?User
129
    {
130
        try {
131
            $userProperties = $this->crypt->decryptArray($token);
132
            /** @var User $user */
133
            /** @var User $userModel */
134
            $userModel = $user::fromArray($userProperties);
135
        } catch (Exception $exception) {
136
            return null;
137
        }
138
139
        return $userModel;
140
    }
141
142
    /**
143
     * Refresh a user from the data store.
144
     *
145
     * @param User $user
146
     *
147
     * @return User
148
     */
149
    public function getFreshUser(User $user): User
150
    {
151
        /** @var User $freshUser */
152
        $freshUser = $this->getUserRepository($user)
153
                          ->findOne($user->__get($user::getIdField()))
154
                          ->getOneOrFail();
155
156
        return $freshUser;
157
    }
158
159
    /**
160
     * Determine if a password verifies against the user's password.
161
     *
162
     * @param User   $user
163
     * @param string $password
164
     *
165
     * @return bool
166
     */
167
    public function isPassword(User $user, string $password): bool
168
    {
169
        return password_verify($password, $user->__get($user::getPasswordField()));
170
    }
171
172
    /**
173
     * Update a user's password.
174
     *
175
     * @param User   $user
176
     * @param string $password
177
     *
178
     * @throws Exception
179
     *
180
     * @return void
181
     */
182
    public function updatePassword(User $user, string $password): void
183
    {
184
        $resetTokenField = $user::getResetTokenField();
185
        /** @var User $dbUser */
186
        $dbUser = $this->getUserRepository($user)
187
                       ->find()
188
                       ->where($resetTokenField, null, $user->__get($resetTokenField))
189
                       ->getOneOrNull();
190
191
        if (! $dbUser) {
0 ignored issues
show
introduced by
$dbUser is of type Valkyrja\Auth\User, thus it always evaluated to true.
Loading history...
192
            throw new InvalidAuthenticationException('Invalid reset token.');
193
        }
194
195
        $dbUser->__set($resetTokenField, null);
196
        $dbUser->__set($user::getPasswordField(), $this->hashPassword($password));
197
198
        $this->saveUser($dbUser);
199
    }
200
201
    /**
202
     * Reset a user's password.
203
     *
204
     * @param User $user
205
     *
206
     * @throws Exception
207
     *
208
     * @return void
209
     */
210
    public function resetPassword(User $user): void
211
    {
212
        $dbUser = $this->getUserViaLoginFields($user);
213
214
        if (! $dbUser) {
0 ignored issues
show
introduced by
$dbUser is of type Valkyrja\Auth\User, thus it always evaluated to true.
Loading history...
215
            throw new InvalidAuthenticationException('No user found.');
216
        }
217
218
        $dbUser->__set($user::getResetTokenField(), Str::random());
219
220
        $this->saveUser($dbUser);
221
    }
222
223
    /**
224
     * Lock a user.
225
     *
226
     * @param LockableUser $user
227
     *
228
     * @return void
229
     */
230
    public function lock(LockableUser $user): void
231
    {
232
        $this->lockUnlock($user, true);
233
    }
234
235
    /**
236
     * Unlock a user.
237
     *
238
     * @param LockableUser $user
239
     *
240
     * @return void
241
     */
242
    public function unlock(LockableUser $user): void
243
    {
244
        $this->lockUnlock($user, false);
245
    }
246
247
    /**
248
     * Register a new user.
249
     *
250
     * @param User $user
251
     *
252
     * @throws InvalidRegistrationException
253
     *
254
     * @return bool
255
     */
256
    public function register(User $user): bool
257
    {
258
        $repository    = $this->getUserRepository($user);
259
        $passwordField = $user::getPasswordField();
260
261
        try {
262
            $user->__set($passwordField, $this->hashPassword($user->__get($passwordField)));
263
264
            $this->orm->ensureTransaction();
265
            $repository->create($user, true);
266
            $repository->persist();
267
268
            return true;
269
        } catch (Exception $exception) {
270
            throw new InvalidRegistrationException($exception->getMessage());
271
        }
272
    }
273
274
    /**
275
     * Determine if a user is registered.
276
     *
277
     * @param User $user
278
     *
279
     * @return bool
280
     */
281
    public function isRegistered(User $user): bool
282
    {
283
        $loginFields = $user::getLoginFields();
284
        $find        = $this->getUserRepository($user)->find();
285
286
        // Iterate through the login fields
287
        foreach ($loginFields as $loginField) {
288
            // Find a user with any of the login fields
289
            $find->orWhere($loginField, null, $user->__get($loginField));
290
        }
291
292
        // If a user is found a user is registered with one of the login fields
293
        if ($find->getOneOrNull()) {
294
            return true;
295
        }
296
297
        return false;
298
    }
299
300
    /**
301
     * Hash a plain password.
302
     *
303
     * @param string $password
304
     *
305
     * @return string
306
     */
307
    protected function hashPassword(string $password): string
308
    {
309
        return password_hash($password, PASSWORD_DEFAULT);
310
    }
311
312
    /**
313
     * Lock or unlock a user.
314
     *
315
     * @param LockableUser $user
316
     * @param bool         $lock
317
     *
318
     * @return void
319
     */
320
    protected function lockUnlock(LockableUser $user, bool $lock): void
321
    {
322
        $user->__set($user::getIsLockedField(), $lock);
323
324
        $this->saveUser($user);
325
    }
326
327
    /**
328
     * Save a user.
329
     *
330
     * @param User $user
331
     *
332
     * @return void
333
     */
334
    protected function saveUser(User $user): void
335
    {
336
        // Get the ORM repository
337
        $repository = $this->getUserRepository($user);
338
339
        $repository->save($user, false);
340
        $repository->persist();
341
    }
342
343
    /**
344
     * Get a user from the DB via login fields.
345
     *
346
     * @param User $user The user to try and get from the db
347
     *
348
     * @return User|null
349
     */
350
    protected function getUserViaLoginFields(User $user): ?User
351
    {
352
        $loginFields = $user::getLoginFields();
353
        $find        = $this->getUserRepository($user)->find();
354
355
        // Iterate through the login fields
356
        foreach ($loginFields as $loginField) {
357
            // Set a where clause for each field
358
            $find->where($loginField, null, $user->__get($loginField));
359
        }
360
361
        /** @var User $dbUser */
362
        $dbUser = $find->getOneOrNull();
363
364
        return $dbUser;
365
    }
366
367
    /**
368
     * Get an ORM repository for the user.
369
     *
370
     * @param User $user The user
371
     *
372
     * @return Repository
373
     */
374
    protected function getUserRepository(User $user): Repository
375
    {
376
        return $this->orm->getRepositoryFromClass($user);
377
    }
378
}
379