Completed
Push — master ( a4be75...a053ce )
by Antonio Carlos
04:13 queued 02:10
created

Authenticator::makeJsonResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
namespace PragmaRX\Google2FALaravel;
4
5
use Carbon\Carbon;
6
use Google2FA;
7
use Illuminate\Http\JsonResponse as IlluminateJsonResponse;
8
use Illuminate\Http\Request;
9
use Illuminate\Http\Response as IlluminateHtmlResponse;
10
use Illuminate\Support\MessageBag;
11
use PragmaRX\Google2FALaravel\Events\OneTimePasswordRequested;
12
use PragmaRX\Google2FALaravel\Exceptions\InvalidOneTimePassword;
13
use PragmaRX\Google2FALaravel\Exceptions\InvalidSecretKey;
14
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
15
16
class Authenticator
17
{
18
    /**
19
     * Constants.
20
     */
21
    const CONFIG_PACKAGE_NAME = 'google2fa';
22
23
    const SESSION_AUTH_PASSED = 'auth_passed';
24
25
    const SESSION_AUTH_TIME = 'auth_time';
26
27
    const SESSION_OTP_TIMESTAMP = 'otp_timestamp';
28
29
    /**
30
     * The auth instance.
31
     *
32
     * @var
33
     */
34
    protected $auth;
35
36
    /**
37
     * The request instance.
38
     *
39
     * @var
40
     */
41
    protected $request;
42
43
    /**
44
     * The current password.
45
     *
46
     * @var
47
     */
48
    protected $password;
49
50
    /**
51
     * Authenticator constructor.
52
     *
53
     * @param Request $request
54
     */
55
    public function __construct(Request $request)
56
    {
57
        $this->setRequest($request);
58
    }
59
60
    /**
61
     * Authenticator boot.
62
     *
63
     * @param $request
64
     *
65
     * @return Authenticator
66
     */
67
    public function boot($request)
68
    {
69
        return $this->setRequest($request);
70
    }
71
72
    /**
73
     * Check if it is already logged in or passable without checking for an OTP.
74
     *
75
     * @return bool
76
     */
77
    protected function canPassWithoutCheckingOTP()
78
    {
79
        return
80
            !$this->isEnabled() ||
81
            $this->noUserIsAuthenticated() ||
82
            $this->twoFactorAuthStillValid();
83
    }
84
85
    /**
86
     * Get a config value.
87
     *
88
     * @param $string
89
     * @param array $children
90
     *
91
     * @throws \Exception
92
     *
93
     * @return mixed
94
     */
95
    protected function config($string, $children = [])
96
    {
97
        if (is_null(config(static::CONFIG_PACKAGE_NAME))) {
98
            throw new \Exception('Config not found');
99
        }
100
101
        return config(
102
            implode('.', array_merge([static::CONFIG_PACKAGE_NAME, $string], (array) $children))
103
        );
104
    }
105
106
    /**
107
     * Create an error bag and store a message on int.
108
     *
109
     * @param $message
110
     *
111
     * @return MessageBag
112
     */
113
    protected function createErrorBagForMessage($message)
114
    {
115
        return new MessageBag([
116
            'message' => $message,
117
        ]);
118
    }
119
120
    /**
121
     * Get or make an auth instance.
122
     *
123
     * @return \Illuminate\Foundation\Application|mixed
124
     */
125
    protected function getAuth()
126
    {
127
        if (is_null($this->auth)) {
128
            $this->auth = app($this->config('auth'));
129
        }
130
131
        return $this->auth;
132
    }
133
134
    /**
135
     * Get a message bag with a message for a particular status code.
136
     *
137
     * @param $statusCode
138
     *
139
     * @return MessageBag
140
     */
141
    protected function getErrorBagForStatusCode($statusCode)
142
    {
143
        return $this->createErrorBagForMessage(
144
            trans(
145
                config(
146
                    $statusCode == SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY
147
                        ? 'google2fa.error_messages.wrong_otp'
148
                        : 'unknown error'
149
                )
150
            )
151
        );
152
    }
153
154
    /**
155
     * Get the user Google2FA secret.
156
     *
157
     * @throws InvalidSecretKey
158
     *
159
     * @return mixed
160
     */
161
    protected function getGoogle2FASecretKey()
162
    {
163
        $secret = $this->getUser()->{$this->config('otp_secret_column')};
164
165
        if (is_null($secret) || empty($secret)) {
166
            throw new InvalidSecretKey('Secret key cannot be empty.');
167
        }
168
169
        return $secret;
170
    }
171
172
    /**
173
     * Get the previous OTP.
174
     *
175
     * @return null|void
176
     */
177
    protected function getOldOneTimePassword()
178
    {
179
        $oldPassword = $this->config('forbid_old_passwords') === true
180
            ? $this->sessionGet(self::SESSION_OTP_TIMESTAMP)
181
            : null;
182
183
        return $oldPassword;
184
    }
185
186
    /**
187
     * Get the OTP from user input.
188
     *
189
     * @throws InvalidOneTimePassword
190
     *
191
     * @return mixed
192
     */
193
    protected function getOneTimePassword()
194
    {
195
        if (!is_null($this->password)) {
196
            return $this->password;
197
        }
198
199
        $this->password = $this->request->input($this->config('otp_input'));
200
201
        if (is_null($this->password) || empty($this->password)) {
202
            throw new InvalidOneTimePassword('One Time Password cannot be empty.');
203
        }
204
205
        return $this->password;
206
    }
207
208
    /**
209
     * Get the request instance.
210
     *
211
     * @return mixed
212
     */
213
    public function getRequest()
214
    {
215
        return $this->request;
216
    }
217
218
    /**
219
     * Get the OTP view.
220
     *
221
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
222
     */
223
    private function getView()
224
    {
225
        return view($this->config('view'));
226
    }
227
228
    /**
229
     * Keep this OTP session alive.
230
     */
231
    protected function keepAlive()
232
    {
233
        if ($this->config('keep_alive')) {
234
            $this->updateCurrentAuthTime();
235
        }
236
    }
237
238
    /**
239
     * Make a session var name for.
240
     *
241
     * @param null $name
242
     *
243
     * @return mixed
244
     */
245
    protected function makeSessionVarName($name = null)
246
    {
247
        return $this->config('session_var').(is_null($name) || empty($name) ? '' : '.'.$name);
248
    }
249
250
    /**
251
     * Check if the request input has the OTP.
252
     *
253
     * @return mixed
254
     */
255
    protected function inputHasOneTimePassword()
256
    {
257
        return $this->request->has($this->config('otp_input'));
258
    }
259
260
    /**
261
     * Make a JSON response.
262
     *
263
     * @param $statusCode
264
     *
265
     * @return JsonResponse
266
     */
267
    protected function makeJsonResponse($statusCode)
268
    {
269
        return new IlluminateJsonResponse(
270
            $this->getErrorBagForStatusCode($statusCode),
271
            $statusCode
272
        );
273
    }
274
275
    /**
276
     * Make the status code, to respond accordingly.
277
     *
278
     * @return int
279
     */
280
    protected function makeStatusCode()
281
    {
282
        return
283
            $this->inputHasOneTimePassword() && !$this->checkOTP()
284
                ? SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY
285
                : SymfonyResponse::HTTP_OK;
286
    }
287
288
    /**
289
     * Make a web response.
290
     *
291
     * @param $statusCode
292
     *
293
     * @return \Illuminate\Http\Response
294
     */
295
    protected function makeHtmlResponse($statusCode)
296
    {
297
        if ($statusCode !== SymfonyResponse::HTTP_OK) {
298
            $this->getView()->withErrors($this->getErrorBagForStatusCode($statusCode));
0 ignored issues
show
Bug introduced by
The method withErrors does only exist in Illuminate\View\View, but not in Illuminate\Contracts\View\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
299
        }
300
301
        return new IlluminateHtmlResponse($this->getView(), $statusCode);
302
    }
303
304
    protected function minutesSinceLastActivity()
305
    {
306
        return Carbon::now()->diffInMinutes(
307
            $this->sessionGet(self::SESSION_AUTH_TIME)
308
        );
309
    }
310
311
    /**
312
     * @return bool
313
     */
314
    protected function noUserIsAuthenticated()
315
    {
316
        return is_null($this->getUser());
317
    }
318
319
    /**
320
     * Check if OTP has expired.
321
     *
322
     * @return bool
323
     */
324
    protected function passwordExpired()
325
    {
326
        if (($minutes = $this->config('lifetime')) == 0 && $this->minutesSinceLastActivity() > $minutes) {
327
            $this->logout();
328
329
            return true;
330
        }
331
332
        $this->keepAlive();
333
334
        return false;
335
    }
336
337
    /**
338
     * Get a session var value.
339
     *
340
     * @param null $var
341
     *
342
     * @return mixed
343
     */
344
    public function sessionGet($var = null)
345
    {
346
        return $this->request->session()->get(
347
            $this->makeSessionVarName($var)
348
        );
349
    }
350
351
    /**
352
     * Put a var value to the current session.
353
     *
354
     * @param $var
355
     * @param $value
356
     *
357
     * @return mixed
358
     */
359
    protected function sessionPut($var, $value)
360
    {
361
        $this->request->session()->put(
362
            $this->makeSessionVarName($var),
363
            $value
364
        );
365
366
        return $value;
367
    }
368
369
    /**
370
     * Forget a session var.
371
     *
372
     * @param null $var
373
     */
374
    protected function sessionForget($var = null)
375
    {
376
        $this->request->session()->forget(
377
            $this->makeSessionVarName($var)
378
        );
379
    }
380
381
    /**
382
     * Set the request property.
383
     *
384
     * @param mixed $request
385
     *
386
     * @return $this
387
     */
388
    public function setRequest($request)
389
    {
390
        $this->request = $request;
391
392
        return $this;
393
    }
394
395
    /**
396
     * Set current auth as valid.
397
     */
398
    protected function storeAuthPassed()
399
    {
400
        $this->sessionPut(self::SESSION_AUTH_PASSED, true);
401
402
        $this->updateCurrentAuthTime();
403
    }
404
405
    /**
406
     * Store the old OTP.
407
     *
408
     * @param $key
409
     *
410
     * @return mixed
411
     */
412
    protected function storeOldOneTimePassord($key)
413
    {
414
        return $this->sessionPut(self::SESSION_OTP_TIMESTAMP, $key);
415
    }
416
417
    /**
418
     * Verifies, in the current session, if a 2fa check has already passed.
419
     *
420
     * @return bool
421
     */
422
    protected function twoFactorAuthStillValid()
423
    {
424
        return
425
            (bool) $this->sessionGet(self::SESSION_AUTH_PASSED, false) &&
0 ignored issues
show
Unused Code introduced by
The call to Authenticator::sessionGet() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
426
            !$this->passwordExpired();
427
    }
428
429
    /**
430
     * Get the current user.
431
     *
432
     * @return mixed
433
     */
434
    protected function getUser()
435
    {
436
        return $this->getAuth()->user();
437
    }
438
439
    /**
440
     * Check if the current use is authenticated via OTP.
441
     *
442
     * @return bool
443
     */
444
    public function isAuthenticated()
445
    {
446
        return
447
            $this->canPassWithoutCheckingOTP()
448
                ? true
449
                : $this->checkOTP();
450
    }
451
452
    /**
453
     * Check if the module is enabled.
454
     *
455
     * @return mixed
456
     */
457
    protected function isEnabled()
458
    {
459
        return $this->config('enabled');
460
    }
461
462
    /**
463
     * Check if the input OTP is valid.
464
     *
465
     * @return bool
466
     */
467
    protected function checkOTP()
468
    {
469
        if (!$this->inputHasOneTimePassword()) {
470
            return false;
471
        }
472
473
        if ($isValid = $this->verifyGoogle2FA()) {
474
            $this->storeAuthPassed();
475
        }
476
477
        return $isValid;
478
    }
479
480
    /**
481
     * OTP logout.
482
     */
483
    public function logout()
484
    {
485
        $this->sessionForget();
486
    }
487
488
    /**
489
     * Create a response to request the OTP.
490
     *
491
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
492
     */
493
    public function makeRequestOneTimePasswordResponse()
494
    {
495
        event(new OneTimePasswordRequested($this->getUser()));
496
497
        return
498
            $this->request->expectsJson()
499
                ? $this->makeJsonResponse($this->makeStatusCode())
500
                : $this->makeHtmlResponse($this->makeStatusCode());
501
    }
502
503
    /**
504
     * Update the current auth time.
505
     */
506
    protected function updateCurrentAuthTime()
507
    {
508
        $this->sessionPut(self::SESSION_AUTH_TIME, Carbon::now());
509
    }
510
511
    /**
512
     * Verify the OTP.
513
     *
514
     * @return mixed
515
     */
516
    protected function verifyGoogle2FA()
517
    {
518
        return $this->storeOldOneTimePassord(
519
            Google2Fa::verifyKey(
520
                $this->getGoogle2FASecretKey(),
521
                $this->getOneTimePassword(),
522
                $this->config('window'),
523
                null, // $timestamp
524
                $this->getOldOneTimePassword()
525
            )
526
        );
527
    }
528
}
529