Completed
Push — master ( d6b1d0...65a0d7 )
by Marceau
02:17
created

AuthChecker   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 269
Duplicated Lines 8.92 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
c 1
b 0
f 0
lcom 1
cbo 9
dl 24
loc 269
rs 8.3999

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A handleLogin() 0 8 2
A handleFailed() 0 5 1
A handleLockout() 0 11 2
A findOrCreateUserDeviceByAgent() 0 11 3
A findUserDeviceByAgent() 12 12 3
A createUserDeviceByAgent() 0 19 4
A findUserFromPayload() 0 14 2
A createUserLoginForDevice() 0 17 1
A findDeviceForUser() 12 12 3
B shouldLogDeviceLogin() 0 17 5
D deviceMatch() 0 27 8
A getDeviceMatchingAttributesConfig() 0 9 1
A getLoginThrottleConfig() 0 4 1
A getLoginColumnConfig() 0 4 1

How to fix   Duplicated Code   

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:

1
<?php
2
3
namespace Lab404\AuthChecker\Services;
4
5
use Carbon\Carbon;
6
use Illuminate\Config\Repository as Config;
7
use Illuminate\Contracts\Auth\Authenticatable;
8
use Illuminate\Foundation\Application;
9
use Illuminate\Http\Request;
10
use Illuminate\Support\Collection;
11
use Jenssegers\Agent\Agent;
12
use Lab404\AuthChecker\Events\DeviceCreated;
13
use Lab404\AuthChecker\Events\LoginCreated;
14
use Lab404\AuthChecker\Models\Device;
15
use Lab404\AuthChecker\Models\Login;
16
17
class AuthChecker
18
{
19
    /** @var Application */
20
    private $app;
21
22
    /** @var Request */
23
    private $request;
24
25
    /** @var Config */
26
    private $config;
27
28
    /**
29
     * AuthChecker
30
     *
31
     * @param Application $app
32
     */
33
    public function __construct(Application $app, Request $request)
0 ignored issues
show
Bug introduced by
You have injected the Request via parameter $request. This is generally not recommended as there might be multiple instances during a request cycle (f.e. when using sub-requests). Instead, it is recommended to inject the RequestStack and retrieve the current request each time you need it via getCurrentRequest().
Loading history...
34
    {
35
        $this->app = $app;
36
        $this->request = $request;
37
        $this->config = $app['config'];
38
    }
39
40
    /**
41
     * @param   Authenticatable $user
42
     * @return  void
43
     */
44
    public function handleLogin(Authenticatable $user)
45
    {
46
        $device = $this->findOrCreateUserDeviceByAgent($user);
47
48
        if ($this->shouldLogDeviceLogin($device)) {
49
            $this->createUserLoginForDevice($user, $device);
50
        }
51
    }
52
53
    /**
54
     * @param   Authenticatable $user
55
     * @return  void
56
     */
57
    public function handleFailed(Authenticatable $user)
58
    {
59
        $device = $this->findOrCreateUserDeviceByAgent($user);
60
        $this->createUserLoginForDevice($user, $device, Login::TYPE_FAILED);
61
    }
62
63
    /**
64
     * @param   array $payload
65
     * @return  void
66
     */
67
    public function handleLockout(array $payload = [])
68
    {
69
        $payload = Collection::make($payload);
70
71
        $user = $this->findUserFromPayload($payload);
72
73
        if ($user) {
74
            $device = $this->findOrCreateUserDeviceByAgent($user);
75
            $this->createUserLoginForDevice($user, $device, Login::TYPE_LOCKOUT);
76
        }
77
    }
78
79
    /**
80
     * @param Authenticatable $user
81
     * @param Agent|null      $agent
82
     * @return Device
83
     */
84
    public function findOrCreateUserDeviceByAgent(Authenticatable $user, Agent $agent = null)
85
    {
86
        $agent = is_null($agent) ? $this->app['agent'] : $agent;
87
        $device = $this->findUserDeviceByAgent($user, $agent);
88
89
        if (is_null($device)) {
90
            $device = $this->createUserDeviceByAgent($user, $agent);
91
        }
92
93
        return $device;
94
    }
95
96
    /**
97
     * @param   Authenticatable $user
98
     * @param   Agent           $agent
99
     * @return  Device|null
100
     */
101 View Code Duplication
    public function findUserDeviceByAgent(Authenticatable $user, Agent $agent)
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...
102
    {
103
        if (!$user->hasDevices()) {
0 ignored issues
show
Bug introduced by
The method hasDevices() does not seem to exist on object<Illuminate\Contracts\Auth\Authenticatable>.

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...
104
            return null;
105
        }
106
107
        $matching = $user->devices->filter(function ($item) use ($agent) {
0 ignored issues
show
Bug introduced by
Accessing devices on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
108
            return $this->deviceMatch($item, $agent);
109
        })->first();
110
111
        return $matching ? $matching : null;
112
    }
113
114
    /**
115
     * @param   Authenticatable $user
116
     * @param   Agent           $agent
117
     * @return  Device
118
     */
119
    public function createUserDeviceByAgent(Authenticatable $user, Agent $agent)
120
    {
121
        $device = new Device();
122
123
        $device->platform = $agent->platform();
124
        $device->platform_version = $agent->version($device->platform);
125
        $device->browser = $agent->browser();
126
        $device->browser_version = $agent->version($device->browser);
127
        $device->is_desktop = $agent->isDesktop() ? true : false;
128
        $device->is_mobile = $agent->isMobile() ? true : false;
129
        $device->language = count($agent->languages()) ? $agent->languages()[0] : null;
130
        $device->user_id = $user->getKey();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Illuminate\Contracts\Auth\Authenticatable as the method getKey() does only exist in the following implementations of said interface: Illuminate\Foundation\Auth\User.

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...
131
132
        $device->save();
133
134
        event(DeviceCreated::class);
135
136
        return $device;
137
    }
138
139
    /**
140
     * @param Collection|null $request
0 ignored issues
show
Bug introduced by
There is no parameter named $request. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
141
     * @return Authenticatable|null
142
     */
143
    public function findUserFromPayload(Collection $payload = null)
144
    {
145
        $login_column = $this->getLoginColumnConfig();
146
147
        if ($payload->has($login_column)) {
148
            $model = $this->config->get('auth.providers.users.model');
149
            $login_value = $payload->get($login_column);
0 ignored issues
show
Bug introduced by
It seems like $payload is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
150
151
            $user = ($model)::where($login_column, '=', $login_value)->first();
152
            return $user;
153
        }
154
155
        return null;
156
    }
157
158
    /**
159
     * @param   Authenticatable $user
160
     * @param   Device          $device
161
     * @param   string          $type
162
     * @return  Login
163
     */
164
    public function createUserLoginForDevice(Authenticatable $user, Device $device, $type = Login::TYPE_LOGIN)
165
    {
166
        $ip = $this->request->ip();
167
168
        $login = new Login([
169
            'user_id' => $user->getKey(),
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Illuminate\Contracts\Auth\Authenticatable as the method getKey() does only exist in the following implementations of said interface: Illuminate\Foundation\Auth\User.

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...
170
            'ip_address' => $ip,
171
            'device_id' => $device->id,
172
            'type' => $type,
173
        ]);
174
175
        $device->login()->save($login);
176
177
        event(new LoginCreated($login));
178
179
        return $login;
180
    }
181
182
    /**
183
     * @param   Authenticatable $user
184
     * @param   Agent           $agent
185
     * @return  false|Device
186
     */
187 View Code Duplication
    public function findDeviceForUser(Authenticatable $user, Agent $agent)
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...
188
    {
189
        if (!$user->hasDevices()) {
0 ignored issues
show
Bug introduced by
The method hasDevices() does not seem to exist on object<Illuminate\Contracts\Auth\Authenticatable>.

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...
190
            return false;
191
        }
192
193
        $device = $user->devices->filter(function ($item) use ($agent) {
0 ignored issues
show
Bug introduced by
Accessing devices on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
194
            return $this->deviceMatch($item, $agent);
195
        })->first();
196
197
        return is_null($device) ? false : $device;
198
    }
199
200
    /**
201
     * @param   Device $device
202
     * @return  bool
203
     */
204
    public function shouldLogDeviceLogin(Device $device)
205
    {
206
        $throttle = $this->getLoginThrottleConfig();
207
208
        if ($throttle === 0 || is_null($device->login)) {
209
            return true;
210
        }
211
212
        $limit = Carbon::now()->subMinutes($throttle);
213
        $login = $device->login;
214
215
        if (isset($login->created_at) && $login->created_at->gt($limit)) {
216
            return false;
217
        }
218
219
        return true;
220
    }
221
222
    /**
223
     * @param   Device $device
224
     * @param   Agent  $agent
225
     * @return  bool
226
     */
227
    public function deviceMatch(Device $device, Agent $agent, array $attributes = null)
228
    {
229
        $attributes = is_null($attributes) ? $this->getDeviceMatchingAttributesConfig() : $attributes;
230
        $matches = count($attributes) > 0 ? false : true;
231
232
        if (in_array('platform', $attributes)) {
233
            $matches = $device->platform === $agent->platform();
234
        }
235
236
        if (in_array('platform_version', $attributes)) {
237
            $matches = $device->platform_version === $agent->version($device->platform);
238
        }
239
240
        if (in_array('browser', $attributes)) {
241
            $matches = $device->browser === $agent->browser();
242
        }
243
244
        if (in_array('browser_version', $attributes)) {
245
            $matches = $device->browser_version === $agent->version($device->browser);
246
        }
247
248
        if (in_array('language', $attributes)) {
249
            $matches = $device->language === $agent->version($device->language);
250
        }
251
252
        return $matches;
253
    }
254
255
    /**
256
     * @param   void
257
     * @return  array
258
     */
259
    public function getDeviceMatchingAttributesConfig()
260
    {
261
        return $this->config->get('laravel-auth-checker.device_matching_attributes', [
262
            'ip',
263
            'platform',
264
            'platform_version',
265
            'browser',
266
        ]);
267
    }
268
269
    /**
270
     * @param   void
271
     * @return  int
272
     */
273
    public function getLoginThrottleConfig()
274
    {
275
        return (int)$this->config->get('laravel-auth-checker.throttle', 0);
276
    }
277
278
    /**
279
     * @return  string
280
     */
281
    public function getLoginColumnConfig()
282
    {
283
        return (string)$this->config->get('laravel-auth-checker.login_column', 'email');
284
    }
285
}
286