Test Failed
Pull Request — master (#350)
by Raffael
26:39
created

Recaptcha   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 146
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 15
lcom 1
cbo 6
dl 0
loc 146
ccs 0
cts 85
cp 0
rs 10
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A setOptions() 0 17 4
A postAuthentication() 0 36 5
B preAuthentication() 0 40 5
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Recaptcha\Hook;
13
14
use Balloon\App\Recaptcha\Exception;
15
use Balloon\Hook\AbstractHook;
16
use InvalidArgumentException;
17
use Micro\Auth\Adapter\None;
18
use Micro\Auth\Auth;
19
use Micro\Auth\Identity;
20
use MongoDB\Database;
21
use Psr\Log\LoggerInterface;
22
use ReCaptcha\ReCaptcha as CaptchaService;
23
24
class Recaptcha extends AbstractHook
25
{
26
    /**
27
     * Logger.
28
     *
29
     * @var LoggerInterface
30
     */
31
    protected $logger;
32
33
    /**
34
     * Recaptcha.
35
     *
36
     * @var CaptchaService
37
     */
38
    protected $recaptcha;
39
40
    /**
41
     * Database.
42
     *
43
     * @var Database
44
     */
45
    protected $db;
46
47
    /**
48
     * Validate captcha.
49
     *
50
     * @var int
51
     */
52
    protected $recaptcha_threshold = 30;
53
54
    /**
55
     * Constructor.
56
     */
57
    public function __construct(LoggerInterface $logger, CaptchaService $recaptcha, Database $db, array $config = [])
58
    {
59
        $this->logger = $logger;
60
        $this->recaptcha = $recaptcha;
61
        $this->db = $db;
62
        $this->setOptions($config);
63
    }
64
65
    /**
66
     * Set options.
67
     */
68
    public function setOptions(array $config = [])
69
    {
70
        foreach ($config as $option => $value) {
71
            switch ($option) {
72
                case 'recaptcha_threshold':
73
                    $this->{$option} = (int) $value;
74
75
                break;
76
                case 'hostname':
77
                    $this->recaptcha->setExpectedHostname($value);
78
79
                break;
80
                default:
81
                    throw new InvalidArgumentException('unknown option '.$option.' given');
82
            }
83
        }
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function postAuthentication(Auth $auth, ?Identity $identity = null): void
90
    {
91
        $username = $_POST['username'] ?? $_SERVER['username'] ?? null;
92
93
        if ($identity === null && $username !== null) {
94
            $this->logger->info('detected unsuccessful authentication for ['.$username.'], increase failed_auth counter', [
95
                'category' => get_class($this),
96
            ]);
97
98
            $this->db->user->updateOne([
99
                'username' => $username,
100
            ], [
101
                '$inc' => [
102
                    'failed_auth' => 1,
103
                ],
104
            ]);
105
106
            return;
107
        }
108
109
        if ($identity instanceof Identity && !($identity->getAdapter() instanceof None)) {
110
            $this->logger->info('detected successful authentication for ['.$identity->getIdentifier().']', [
111
                'category' => get_class($this),
112
            ]);
113
114
            $this->db->user->updateOne([
115
                'username' => $username,
116
            ], [
117
                '$set' => [
118
                    'failed_auth' => 0,
119
                ],
120
            ]);
121
122
            return;
123
        }
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129
    public function preAuthentication(Auth $auth): void
130
    {
131
        $username = $_POST['username'] ?? $_SERVER['username'] ?? null;
132
133
        if ($username === null) {
134
            return;
135
        }
136
137
        $result = $this->db->user->findOne([
138
            'username' => $username,
139
            'failed_auth' => [
140
                '$gte' => $this->recaptcha_threshold,
141
            ],
142
        ]);
143
144
        if ($result !== null) {
145
            $this->logger->info('max failed auth requests for user ['.$username.'] exceeded recaptcha threshold ['.$this->recaptcha_threshold.']', [
146
                'category' => get_class($this),
147
            ]);
148
149
            if (!isset($_GET['g-recaptcha-response'])) {
150
                throw new Exception\InvalidRecaptchaToken('valid recaptcha token `g-recaptcha-response` is required');
151
            }
152
153
            $resp = $this->recaptcha->verify($_GET['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
154
155
            if ($resp->isSuccess()) {
156
                $this->logger->info('recaptcha token validation for ['.$username.'] succeeded', [
157
                    'category' => get_class($this),
158
                ]);
159
            } else {
160
                $this->logger->info('recaptcha token validation for ['.$username.'] failed', [
161
                    'category' => get_class($this),
162
                    'errors' => $resp->getErrorCodes(),
163
                ]);
164
165
                throw new Exception\InvalidRecaptchaToken('Valid recaptcha token `g-recaptcha-response` is required');
166
            }
167
        }
168
    }
169
}
170