AuthChecker::findOrCreateUserDeviceByAgent()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 3
nc 4
nop 2
1
<?php
2
3
namespace Lab404\AuthChecker\Services;
4
5
use Carbon\Carbon;
6
use Illuminate\Config\Repository as Config;
7
use Illuminate\Database\Eloquent\Builder;
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\FailedAuth;
14
use Lab404\AuthChecker\Events\LockoutAuth;
15
use Lab404\AuthChecker\Events\LoginCreated;
16
use Lab404\AuthChecker\Interfaces\HasLoginsAndDevicesInterface;
17
use Lab404\AuthChecker\Models\Device;
18
use Lab404\AuthChecker\Models\Login;
19
20
class AuthChecker
21
{
22
    /** @var Application $app */
23
    private $app;
24
    /** @var Request $request */
25
    private $request;
26
    /** @var Config $config */
27
    private $config;
28
29
    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...
30
    {
31
        $this->app = $app;
32
        $this->request = $request;
33
        $this->config = $app['config'];
34
    }
35
36
    public function handleLogin(HasLoginsAndDevicesInterface $user): void
37
    {
38
        $device = $this->findOrCreateUserDeviceByAgent($user);
39
40
        if ($this->shouldLogDeviceLogin($device)) {
41
            $this->createUserLoginForDevice($user, $device);
42
        }
43
    }
44
45
    public function handleFailed(HasLoginsAndDevicesInterface $user): void
46
    {
47
        $device = $this->findOrCreateUserDeviceByAgent($user);
48
        $this->createUserLoginForDevice($user, $device, Login::TYPE_FAILED);
49
50
        event(new FailedAuth($device->login, $device));
51
    }
52
53
    public function handleLockout(array $payload = []): void
54
    {
55
        $payload = Collection::make($payload);
56
57
        $user = $this->findUserFromPayload($payload);
58
59
        if ($user) {
60
            $device = $this->findOrCreateUserDeviceByAgent($user);
61
            $this->createUserLoginForDevice($user, $device, Login::TYPE_LOCKOUT);
62
63
            event(new LockoutAuth($device->login, $device));
64
        }
65
    }
66
67
    public function findOrCreateUserDeviceByAgent(HasLoginsAndDevicesInterface $user, Agent $agent = null): Device
68
    {
69
        $agent = is_null($agent) ? $this->app['agent'] : $agent;
70
        $device = $this->findUserDeviceByAgent($user, $agent);
71
72
        if (is_null($device)) {
73
            $device = $this->createUserDeviceByAgent($user, $agent);
74
        }
75
76
        return $device;
77
    }
78
79
    public function findUserDeviceByAgent(HasLoginsAndDevicesInterface $user, Agent $agent): ?Device
80
    {
81
        if (!$user->hasDevices()) {
82
            return null;
83
        }
84
85
        $matching = $user->devices->filter(function ($item) use ($agent) {
0 ignored issues
show
Bug introduced by
Accessing devices on the interface Lab404\AuthChecker\Inter...ginsAndDevicesInterface 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...
86
            return $this->deviceMatch($item, $agent);
87
        })->first();
88
89
        return $matching ? $matching : null;
90
    }
91
92
    public function createUserDeviceByAgent(HasLoginsAndDevicesInterface $user, Agent $agent): Device
93
    {
94
        $model = config('auth-checker.models.device') ?? Device::class;
95
        $device = new $model;
96
97
        $device->platform = $agent->platform();
98
        $device->platform_version = $agent->version($device->platform);
99
        $device->browser = $agent->browser();
100
        $device->browser_version = $agent->version($device->browser);
101
        $device->is_desktop = $agent->isDesktop() ? true : false;
102
        $device->is_mobile = $agent->isMobile() ? true : false;
103
        $device->language = count($agent->languages()) ? $agent->languages()[0] : null;
104
105
        $device->user()->associate($user);
106
107
        $device->save();
108
109
        event(new DeviceCreated($device));
110
111
        return $device;
112
    }
113
114
    public function findUserFromPayload(Collection $payload): ?HasLoginsAndDevicesInterface
115
    {
116
        $login_column = $this->getLoginColumnConfig();
117
118
        if ($payload->has($login_column)) {
119
            $model = (string)$this->config->get('auth.providers.users.model');
120
            $login_value = $payload->get($login_column);
121
122
            /** @var Builder $model */
123
            $user = $model::where($login_column, '=', $login_value)->first();
124
            return $user;
125
        }
126
127
        return null;
128
    }
129
130
    public function createUserLoginForDevice(
131
        HasLoginsAndDevicesInterface $user,
132
        Device $device,
133
        string $type = Login::TYPE_LOGIN
134
    ): Login {
135
        $model = config('auth-checker.models.login') ?? Login::class;
136
        $ip = $this->request->ip();
137
138
        $login = new $model([
139
            'ip_address' => $ip,
140
            'device_id' => $device->id,
141
            'type' => $type,
142
        ]);
143
144
        $login->user()->associate($user);
145
146
        $device->login()->save($login);
147
148
        event(new LoginCreated($login));
149
150
        return $login;
151
    }
152
153
    public function findDeviceForUser(HasLoginsAndDevicesInterface $user, Agent $agent): ?Device
154
    {
155
        if (!$user->hasDevices()) {
156
            return false;
157
        }
158
159
        $device = $user->devices->filter(function ($item) use ($agent) {
0 ignored issues
show
Bug introduced by
Accessing devices on the interface Lab404\AuthChecker\Inter...ginsAndDevicesInterface 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...
160
            return $this->deviceMatch($item, $agent);
161
        })->first();
162
163
        return is_null($device) ? false : $device;
164
    }
165
166
    public function shouldLogDeviceLogin(Device $device): bool
167
    {
168
        $throttle = $this->getLoginThrottleConfig();
169
170
        if ($throttle === 0 || is_null($device->login)) {
171
            return true;
172
        }
173
174
        $limit = Carbon::now()->subMinutes($throttle);
175
        $login = $device->login;
176
177
        if (isset($login->created_at) && $login->created_at->gt($limit)) {
178
            return false;
179
        }
180
181
        return true;
182
    }
183
184
    public function deviceMatch(Device $device, Agent $agent, array $attributes = null): bool
185
    {
186
        $attributes = is_null($attributes) ? $this->getDeviceMatchingAttributesConfig() : $attributes;
187
        $matches = 0;
188
189
        if (in_array('platform', $attributes)) {
190
            $matches += $device->platform === $agent->platform();
191
        }
192
193
        if (in_array('platform_version', $attributes)) {
194
            $agentPlatformVersion = $agent->version($device->platform);
195
            $agentPlatformVersion = empty($agentPlatformVersion) ? '0' : $agentPlatformVersion;
196
            $matches += $device->platform_version === $agentPlatformVersion;
197
        }
198
199
        if (in_array('browser', $attributes)) {
200
            $matches += $device->browser === $agent->browser();
201
        }
202
203
        if (in_array('browser_version', $attributes)) {
204
            $matches += $device->browser_version === $agent->version($device->browser);
205
        }
206
207
        if (in_array('language', $attributes)) {
208
            $matches += $device->language === $agent->version($device->language);
209
        }
210
211
        return $matches === count($attributes);
212
    }
213
214
    public function getDeviceMatchingAttributesConfig(): array
215
    {
216
        return $this->config->get('auth-checker.device_matching_attributes', [
217
            'platform',
218
            'platform_version',
219
            'browser',
220
        ]);
221
    }
222
223
    public function getLoginThrottleConfig(): int
224
    {
225
        return (int)$this->config->get('auth-checker.throttle', 0);
226
    }
227
228
    public function getLoginColumnConfig(): string
229
    {
230
        return (string)$this->config->get('auth-checker.login_column', 'email');
231
    }
232
}
233