Completed
Push — master ( b6640d...1255af )
by Mathieu
04:15
created

AuthToken   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 369
Duplicated Lines 2.71 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 42
c 1
b 0
f 0
lcom 1
cbo 3
dl 10
loc 369
rs 8.295

22 Methods

Rating   Name   Duplication   Size   Complexity  
A key() 0 4 1
A setIdent() 0 5 1
A ident() 0 4 1
A setToken() 0 5 1
A token() 0 4 1
A setUsername() 10 10 2
A username() 0 4 1
A setExpiry() 0 17 4
A expiry() 0 4 1
A setCreated() 0 17 4
A created() 0 4 1
A setLastModified() 0 17 4
A lastModified() 0 4 1
A generate() 0 9 1
A sendCookie() 0 11 1
A preSave() 0 12 2
A preUpdate() 0 8 1
A getTokenDataFromCookie() 0 19 4
A getUserId() 0 4 1
B getUsernameFromToken() 0 26 5
A panic() 0 21 2
A createMetadata() 0 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AuthToken often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AuthToken, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Charcoal\User;
4
5
use \DateTime;
6
use \DateTimeInterface;
7
use \InvalidArgumentException;
8
9
use \Charcoal\Model\AbstractModel;
10
11
use \Charcoal\User\AuthTokenMetadata;
12
13
/**
14
 * Authorization token; to keep a user logged in
15
 */
16
class AuthToken extends AbstractModel
17
{
18
19
    /**
20
     * @var string $ident
21
     */
22
    private $ident;
23
24
    /**
25
     * @var string $token
26
     */
27
    private $token;
28
29
    /**
30
     * The username should be unique and mandatory.
31
     * @var string $username
32
     */
33
    private $username;
34
35
    /**
36
     * @var Datetime $expiry
37
     */
38
    private $expiry;
39
40
    /**
41
     * Token creation date (set automatically on save)
42
     * @var DateTime $Created
43
     */
44
    private $created;
45
46
    /**
47
     * Token last modified date (set automatically on save and update)
48
     * @var DateTime $LastModified
49
     */
50
    private $lastModified;
51
52
    /**
53
     * @return string
54
     */
55
    public function key()
56
    {
57
        return 'ident';
58
    }
59
60
    /**
61
     * @param string $ident The token ident.
62
     * @return AuthToken Chainable
63
     */
64
    public function setIdent($ident)
65
    {
66
        $this->ident = $ident;
67
        return $this;
68
    }
69
70
    /**
71
     * @return string
72
     */
73
    public function ident()
74
    {
75
        return $this->ident;
76
    }
77
78
    /**
79
     * @param string $token The token.
80
     * @return AuthToken Chainable
81
     */
82
    public function setToken($token)
83
    {
84
        $this->token = $token;
85
        return $this;
86
    }
87
88
    /**
89
     * @return string
90
     */
91
    public function token()
92
    {
93
        return $this->token;
94
    }
95
96
97
    /**
98
     * Force a lowercase username
99
     *
100
     * @param string $username The username (also the login name).
101
     * @throws InvalidArgumentException If the username is not a string.
102
     * @return User Chainable
103
     */
104 View Code Duplication
    public function setUsername($username)
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...
105
    {
106
        if (!is_string($username)) {
107
            throw new InvalidArgumentException(
108
                'Set user username: Username must be a string'
109
            );
110
        }
111
        $this->username = mb_strtolower($username);
112
        return $this;
113
    }
114
115
    /**
116
     * @return string
117
     */
118
    public function username()
119
    {
120
        return $this->username;
121
    }
122
123
    /**
124
     * @param DateTime|string|null $expiry The date/time at object's creation.
125
     * @throws InvalidArgumentException If the date/time is invalid.
126
     * @return Content Chainable
127
     */
128
    public function setExpiry($expiry)
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $expiry of type object<DateTime> is incompatible with the declared type object<Charcoal\User\Datetime> of property $expiry.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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 DateTime|string|null $created The date/time at object's creation.
156
     * @throws InvalidArgumentException If the date/time is invalid.
157
     * @return Content Chainable
158
     */
159
    public function setCreated($created)
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 DateTime|null
179
     */
180
    public function created()
181
    {
182
        return $this->created;
183
    }
184
185
    /**
186
     * @param DateTime|string|null $lastModified The last modified date/time.
187
     * @throws InvalidArgumentException If the date/time is invalid.
188
     * @return Content Chainable
189
     */
190
    public function setLastModified($lastModified)
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 DateTime
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 $username The username to generate the auth token from.
220
     * @return AuthToken Chainable
221
     */
222
    public function generate($username)
223
    {
224
        $this->setIdent(bin2hex(random_bytes(16)));
225
        $this->setToken(bin2hex(random_bytes(32)));
226
        $this->setUsername($username);
227
        $this->setExpiry('now + '.$this->metadata()->cookieDuration());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Model\MetadataInterface as the method cookieDuration() does only exist in the following implementations of said interface: Charcoal\User\AuthTokenMetadata.

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...
228
229
        return $this;
230
    }
231
232
    /**
233
     * @return AuthToken Chainable
234
     */
235
    public function sendCookie()
236
    {
237
        $cookieName = $this->metadata()->cookieName();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Model\MetadataInterface as the method cookieName() does only exist in the following implementations of said interface: Charcoal\User\AuthTokenMetadata.

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...
238
        $value = $this->ident().';'.$this->token();
239
        $expiry = $this->expiry()->getTimestamp();
240
        $secure = $this->metadata()->httpsOnly();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Model\MetadataInterface as the method httpsOnly() does only exist in the following implementations of said interface: Charcoal\User\AuthTokenMetadata.

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...
241
242
        setcookie($cookieName, $value, $expiry, '', '', $secure);
243
244
        return $this;
245
    }
246
247
     /**
248
      * StorableTrait > preSave(): Called automatically before saving the object to source.
249
      * @return boolean
250
      */
251
    public function preSave()
252
    {
253
        parent::preSave();
254
255
        if (password_needs_rehash($this->token, PASSWORD_DEFAULT)) {
256
            $this->token = password_hash($this->token, PASSWORD_DEFAULT);
257
        }
258
        $this->setCreated('now');
259
        $this->setLastModified('now');
260
261
        return true;
262
    }
263
264
    /**
265
     * StorableTrait > preUpdate(): Called automatically before updating the object to source.
266
     * @param array $properties The properties (ident) set for update.
267
     * @return boolean
268
     */
269
    public function preUpdate(array $properties = null)
270
    {
271
        parent::preUpdate($properties);
0 ignored issues
show
Bug introduced by
It seems like $properties defined by parameter $properties on line 269 can also be of type array; however, Charcoal\Model\AbstractModel::preUpdate() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
272
273
        $this->setLastModified('now');
274
275
        return true;
276
    }
277
278
    /**
279
     * @return array `['ident'=>'', 'token'=>'']
280
     */
281
    public function getTokenDataFromCookie()
0 ignored issues
show
Coding Style introduced by
getTokenDataFromCookie uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
282
    {
283
        $cookieName = $this->metadata()->cookieName();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Model\MetadataInterface as the method cookieName() does only exist in the following implementations of said interface: Charcoal\User\AuthTokenMetadata.

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...
284
285
        if (!isset($_COOKIE[$cookieName])) {
286
            return null;
287
        }
288
289
        $authCookie = $_COOKIE[$cookieName];
290
        $vals = explode(';', $authCookie);
291
        if (!isset($vals[0]) || !isset($vals[1])) {
292
            return null;
293
        }
294
295
        return [
296
            'ident' => $vals[0],
297
            'token' => $vals[1]
298
        ];
299
    }
300
301
    /**
302
     * @param mixed  $ident The auth-token identifier.
303
     * @param string $token The token key to validate against.
304
     * @return mixed The user id.
305
     */
306
    public function getUserId($ident, $token)
307
    {
308
        return $this->getUsernameFromToken($ident, $token);
309
    }
310
311
    /**
312
     * @param mixed  $ident The auth-token identifier (username).
313
     * @param string $token The token to validate against.
314
     * @return mixed The user id. An empty string if no token match.
315
     */
316
    public function getUsernameFromToken($ident, $token)
317
    {
318
        $this->load($ident);
319
        if (!$this->ident()) {
320
            $this->logger->warning(sprintf('Auth token not found: "%s"', $ident));
321
            return '';
322
        }
323
324
        // Expired cookie
325
        $now = new DateTime('now');
326
        if (!$this->expiry() || $now > $this->expiry()) {
327
            $this->logger->warning('Expired auth token');
328
            $this->delete();
329
            return '';
330
        }
331
332
        // Validate encrypted token
333
        if (password_verify($token, $this->token()) !== true) {
334
            $this->panic();
335
            $this->delete();
336
            return '';
337
        }
338
339
        // Success!
340
        return $this->username();
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
        // Todo: delete all user's token.
351
        // Gve a strongly-worded error message.
352
353
        $this->logger->error(
354
            'Possible security breach: an authentication token was found in the database but its token does not match.'
355
        );
356
357
        if ($this->username) {
358
            $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...
359
            $q = '
360
            delete from
361
                '.$table.'
362
            where
363
                username = :username';
364
            $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...
365
                'username'=>$this->username()
366
            ]);
367
        }
368
    }
369
370
    /**
371
     * DescribableTrait > create_metadata().
372
     *
373
     * @param array $data Optional data to intialize the Metadata object with.
374
     * @return MetadataInterface
375
     */
376
    protected function createMetadata(array $data = null)
377
    {
378
        $metadata = new AuthTokenMetadata();
379
        if ($data !== null) {
380
            $metadata->setData($data);
0 ignored issues
show
Unused Code introduced by
The call to the method Charcoal\User\AuthTokenMetadata::setData() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
381
        }
382
        return $metadata;
383
    }
384
}
385