Passed
Push — master ( 388e0b...41d6d2 )
by
unknown
50s queued 10s
created

AuthToken::setUserId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace Charcoal\User;
4
5
use DateTime;
6
use DateTimeInterface;
7
use InvalidArgumentException;
8
9
// From 'charcoal-core'
10
use Charcoal\Model\AbstractModel;
11
12
// From 'charcoal-user'
13
use Charcoal\User\AuthTokenMetadata;
14
15
/**
16
 * Authorization token; to keep a user logged in
17
 */
18
class AuthToken extends AbstractModel
0 ignored issues
show
Bug introduced by
There is at least one abstract method in this class. Maybe declare it as abstract, or implement the remaining methods: hasProperty, p, properties, property
Loading history...
19
{
20
    /**
21
     * @var string
22
     */
23
    private $ident;
24
25
    /**
26
     * @var string
27
     */
28
    private $token;
29
30
    /**
31
     * The user ID should be unique and mandatory.
32
     *
33
     * @var string
34
     */
35
    private $userId;
36
37
    /**
38
     * @var DateTimeInterface|null
39
     */
40
    private $expiry;
41
42
    /**
43
     * Token creation date (set automatically on save)
44
     * @var DateTimeInterface|null
45
     */
46
    private $created;
47
48
    /**
49
     * Token last modified date (set automatically on save and update)
50
     * @var DateTimeInterface|null
51
     */
52
    private $lastModified;
53
54
    /**
55
     * @return string
56
     */
57
    public function key()
58
    {
59
        return 'ident';
60
    }
61
62
    /**
63
     * @param string $ident The token ident.
64
     * @return AuthToken Chainable
65
     */
66
    public function setIdent($ident)
67
    {
68
        $this->ident = $ident;
69
        return $this;
70
    }
71
72
    /**
73
     * @return string
74
     */
75
    public function ident()
76
    {
77
        return $this->ident;
78
    }
79
80
    /**
81
     * @param string $token The token.
82
     * @return AuthToken Chainable
83
     */
84
    public function setToken($token)
85
    {
86
        $this->token = $token;
87
        return $this;
88
    }
89
90
    /**
91
     * @return string
92
     */
93
    public function token()
94
    {
95
        return $this->token;
96
    }
97
98
99
    /**
100
     * @param string $id The user ID.
101
     * @throws InvalidArgumentException If the user ID is not a string.
102
     * @return AuthToken Chainable
103
     */
104
    public function setUserId($id)
105
    {
106
        if (!is_string($id)) {
107
            throw new InvalidArgumentException(
108
                'Set User ID: identifier must be a string'
109
            );
110
        }
111
        $this->userId = $id;
112
        return $this;
113
    }
114
115
    /**
116
     * @return string
117
     */
118
    public function userId()
119
    {
120
        return $this->userId;
121
    }
122
123
    /**
124
     * @param DateTimeInterface|string|null $expiry The date/time at object's creation.
125
     * @throws InvalidArgumentException If the date/time is invalid.
126
     * @return AuthToken Chainable
127
     */
128 View Code Duplication
    public function setExpiry($expiry)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
129
    {
130
        if ($expiry === null) {
131
            $this->expiry = null;
132
            return $this;
133
        }
134
        if (is_string($expiry)) {
135
            $expiry = new DateTime($expiry);
136
        }
137
        if (!($expiry instanceof DateTimeInterface)) {
138
            throw new InvalidArgumentException(
139
                'Invalid "Expiry" value. Must be a date/time string or a DateTime object.'
140
            );
141
        }
142
        $this->expiry = $expiry;
143
        return $this;
144
    }
145
146
    /**
147
     * @return DateTimeInterface|null
148
     */
149
    public function expiry()
150
    {
151
        return $this->expiry;
152
    }
153
154
    /**
155
     * @param DateTimeInterface|string|null $created The date/time at object's creation.
156
     * @throws InvalidArgumentException If the date/time is invalid.
157
     * @return AuthToken Chainable
158
     */
159 View Code Duplication
    public function setCreated($created)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
160
    {
161
        if ($created === null) {
162
            $this->created = null;
163
            return $this;
164
        }
165
        if (is_string($created)) {
166
            $created = new DateTime($created);
167
        }
168
        if (!($created instanceof DateTimeInterface)) {
169
            throw new InvalidArgumentException(
170
                'Invalid "Created" value. Must be a date/time string or a DateTime object.'
171
            );
172
        }
173
        $this->created = $created;
174
        return $this;
175
    }
176
177
    /**
178
     * @return DateTimeInterface|null
179
     */
180
    public function created()
181
    {
182
        return $this->created;
183
    }
184
185
    /**
186
     * @param DateTimeInterface|string|null $lastModified The last modified date/time.
187
     * @throws InvalidArgumentException If the date/time is invalid.
188
     * @return AuthToken Chainable
189
     */
190 View Code Duplication
    public function setLastModified($lastModified)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
191
    {
192
        if ($lastModified === null) {
193
            $this->lastModified = null;
194
            return $this;
195
        }
196
        if (is_string($lastModified)) {
197
            $lastModified = new DateTime($lastModified);
198
        }
199
        if (!($lastModified instanceof DateTimeInterface)) {
200
            throw new InvalidArgumentException(
201
                'Invalid "Last Modified" value. Must be a date/time string or a DateTime object.'
202
            );
203
        }
204
        $this->lastModified = $lastModified;
205
        return $this;
206
    }
207
208
    /**
209
     * @return DateTimeInterface|null
210
     */
211
    public function lastModified()
212
    {
213
        return $this->lastModified;
214
    }
215
216
    /**
217
     * Note: the `random_bytes()` function is new to PHP-7. Available in PHP 5 with `compat-random`.
218
     *
219
     * @param string $userId The user ID to generate the auth token from.
220
     * @return AuthToken Chainable
221
     */
222
    public function generate($userId)
223
    {
224
        $metadata = $this->metadata();
225
        $this->setIdent(bin2hex(random_bytes(16)));
226
        $this->setToken(bin2hex(random_bytes(32)));
227
        $this->setUserId($userId);
228
        $this->setExpiry('now + '.$metadata['cookieDuration']);
229
230
        return $this;
231
    }
232
233
    /**
234
     * @return AuthToken Chainable
235
     */
236
    public function sendCookie()
237
    {
238
        $metadata = $this->metadata();
239
        $cookieName = $metadata['cookieName'];
240
        $value = $this->ident().';'.$this->token();
241
        $expiry = $this->expiry() ? $this->expiry()->getTimestamp() : null;
242
        $secure = $metadata['httpsOnly'];
243
244
        setcookie($cookieName, $value, $expiry, '', '', $secure);
245
246
        return $this;
247
    }
248
249
    /**
250
     * @return array|null `['ident'=>'', 'token'=>'']
251
     */
252
    public function getTokenDataFromCookie()
253
    {
254
        $metadata = $this->metadata();
255
        $cookieName = $metadata['cookieName'];
256
257
        if (!isset($_COOKIE[$cookieName])) {
258
            return null;
259
        }
260
261
        $authCookie = $_COOKIE[$cookieName];
262
        $vals = explode(';', $authCookie);
263
        if (!isset($vals[0]) || !isset($vals[1])) {
264
            return null;
265
        }
266
267
        return [
268
            'ident' => $vals[0],
269
            'token' => $vals[1]
270
        ];
271
    }
272
273
    /**
274
     * @param mixed  $ident The auth-token identifier.
275
     * @param string $token The token to validate against.
276
     * @return mixed The user id. An empty string if no token match.
277
     */
278
    public function getUserIdFromToken($ident, $token)
279
    {
280
        if (!$this->source()->tableExists()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method tableExists() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
281
            return '';
282
        }
283
284
        $this->load($ident);
285
        if (!$this->ident()) {
286
            $this->logger->warning(sprintf(
287
                'Auth token not found: "%s"',
288
                $ident
289
            ));
290
            return '';
291
        }
292
293
        // Expired cookie
294
        $now = new DateTime('now');
295
        if (!$this->expiry() || $now > $this->expiry()) {
296
            $this->logger->warning('Expired auth token');
297
            $this->delete();
298
            return '';
299
        }
300
301
        // Validate encrypted token
302
        if (password_verify($token, $this->token()) !== true) {
303
            $this->panic();
304
            $this->delete();
305
            return '';
306
        }
307
308
        // Success!
309
        return $this->userId();
310
    }
311
312
    /**
313
     * StorableTrait > preSave(): Called automatically before saving the object to source.
314
     * @return boolean
315
     */
316
    protected function preSave()
317
    {
318
        parent::preSave();
319
320
        if (password_needs_rehash($this->token, PASSWORD_DEFAULT)) {
321
            $this->token = password_hash($this->token, PASSWORD_DEFAULT);
322
        }
323
        $this->setCreated('now');
324
        $this->setLastModified('now');
325
326
        return true;
327
    }
328
329
    /**
330
     * StorableTrait > preUpdate(): Called automatically before updating the object to source.
331
     * @param array $properties The properties (ident) set for update.
332
     * @return boolean
333
     */
334
    protected function preUpdate(array $properties = null)
335
    {
336
        parent::preUpdate($properties);
337
338
        $this->setLastModified('now');
339
340
        return true;
341
    }
342
343
    /**
344
     * Something is seriously wrong: a cookie ident was in the database but with a tampered token.
345
     *
346
     * @return void
347
     */
348
    protected function panic()
349
    {
350
        $this->logger->error(
351
            'Possible security breach: an authentication token was found in the database but its token does not match.'
352
        );
353
354
        if ($this->userId) {
355
            $table = $this->source()->table();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method table() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
356
            $q = sprintf('
357
                DELETE FROM
358
                    `%s`
359
                WHERE
360
                    userId = :userId', $table);
361
            $this->source()->dbQuery($q, [
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method dbQuery() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
362
                'userId' => $this->userId()
363
            ]);
364
        }
365
    }
366
367
    /**
368
     * Create a new metadata object.
369
     *
370
     * @param  array $data Optional metadata to merge on the object.
371
     * @return AuthTokenMetadata
372
     */
373
    protected function createMetadata(array $data = null)
374
    {
375
        $class = $this->metadataClass();
376
        return new $class($data);
377
    }
378
379
    /**
380
     * Retrieve the class name of the metadata object.
381
     *
382
     * @return string
383
     */
384
    protected function metadataClass()
385
    {
386
        return AuthTokenMetadata::class;
387
    }
388
}
389