Passed
Push — master ( 004729...b97da3 )
by Tim
02:36
created

OTP::setTextUtils()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace SimpleSAML\Module\cmdotcom\Controller;
4
5
use CMText\TextClientStatusCodes;
6
use RuntimeException;
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Auth;
9
use SimpleSAML\Configuration;
10
use SimpleSAML\Error;
11
use SimpleSAML\HTTP\RunnableResponse;
12
use SimpleSAML\Logger;
13
use SimpleSAML\Module;
14
use SimpleSAML\Module\cmdotcom\Utils\TextMessage as TextUtils;
15
use SimpleSAML\Module\cmdotcom\Utils\Random as RandomUtils;
16
use SimpleSAML\Session;
17
use SimpleSAML\Utils;
18
use SimpleSAML\XHTML\Template;
19
use Symfony\Component\HttpFoundation\RedirectResponse;
20
use Symfony\Component\HttpFoundation\Request;
21
use UnexpectedValueException;
22
23
/**
24
 * Controller class for the cmdotcom module.
25
 *
26
 * This class serves the verification code and error views available in the module.
27
 *
28
 * @package SimpleSAML\Module\cmdotcom
29
 */
30
class OTP
31
{
32
    /** @var \SimpleSAML\Configuration */
33
    protected Configuration $config;
34
35
    /** @var \SimpleSAML\Logger */
36
    protected Logger $logger;
37
38
    /** @var \SimpleSAML\Configuration */
39
    protected Configuration $moduleConfig;
40
41
    /** @var \SimpleSAML\Session */
42
    protected Session $session;
43
44
    /** @var \SimpleSAML\Utils\HTTP */
45
    protected Utils\HTTP $httpUtils;
46
47
    /** @var \SimpleSAML\Module\cmdotcom\Utils\TextMessage */
48
    protected TextUtils $textUtils;
49
50
    /** @var \SimpleSAML\Module\cmdotcom\Utils\Random */
51
    protected RandomUtils $randomUtils;
52
53
    /**
54
     * @var \SimpleSAML\Auth\State|string
55
     * @psalm-var \SimpleSAML\Auth\State|class-string
56
     */
57
    protected $authState = Auth\State::class;
58
59
60
    /**
61
     * OTP Controller constructor.
62
     *
63
     * @param \SimpleSAML\Configuration $config The configuration to use.
64
     * @param \SimpleSAML\Session $session The current user session.
65
     */
66
    public function __construct(Configuration $config, Session $session)
67
    {
68
        $this->config = $config;
69
        $this->httpUtils = new Utils\HTTP();
70
        $this->textUtils = new TextUtils();
71
        $this->randomUtils = new RandomUtils();
72
        $this->moduleConfig = Configuration::getConfig('module_cmdotcom.php');
73
        $this->session = $session;
74
    }
75
76
77
    /**
78
     * Inject the \SimpleSAML\Logger dependency.
79
     *
80
     * @param \SimpleSAML\Logger $logger
81
     */
82
    public function setLogger(Logger $logger): void
83
    {
84
        $this->logger = $logger;
85
    }
86
87
88
    /**
89
     * Inject the \SimpleSAML\Utils\HTTP dependency.
90
     *
91
     * @param \SimpleSAML\Utils\HTTP $httpUtils
92
     */
93
    public function setHttpUtils(Utils\HTTP $httpUtils): void
94
    {
95
        $this->httpUtils = $httpUtils;
96
    }
97
98
99
    /**
100
     * Inject the \SimpleSAML\Module\cmdotcom\Utils\OTP dependency.
101
     *
102
     * @param \SimpleSAML\Module\cmdotcom\Utils\TextMessage $textUtils
103
     */
104
    public function setTextUtils(TextUtils $textUtils): void
105
    {
106
        $this->textUtils = $textUtils;
107
    }
108
109
110
    /**
111
     * Inject the \SimpleSAML\Auth\State dependency.
112
     *
113
     * @param \SimpleSAML\Auth\State $authState
114
     */
115
    public function setAuthState(Auth\State $authState): void
116
    {
117
        $this->authState = $authState;
118
    }
119
120
121
    /**
122
     * Display the page where the validation code should be entered.
123
     *
124
     * @return \SimpleSAML\XHTML\Template
125
     */
126
    public function enterCode(Request $request): Template
127
    {
128
        $id = $request->get('AuthState', null);
129
        if ($id === null) {
130
            throw new Error\BadRequest('Missing AuthState parameter.');
131
        }
132
133
        $this->authState::loadState($id, 'cmdotcom:request');
134
135
        $t = new Template($this->config, 'cmdotcom:entercode.twig');
136
        $t->data = [
137
            'AuthState' => $id,
138
            'stateparams' => [],
139
        ];
140
141
        return $t;
142
    }
143
144
145
    /**
146
     * Process the entered validation code.
147
     *
148
     * @return \SimpleSAML\HTTP\RunnableResponse
149
     */
150
    public function validateCode(Request $request): RunnableResponse
151
    {
152
        $id = $request->get('AuthState', null);
153
        if ($id === null) {
154
            throw new Error\BadRequest('Missing AuthState parameter.');
155
        }
156
157
        $state = $this->authState::loadState($id, 'cmdotcom:request');
158
159
        Assert::keyExists($state, 'cmdotcom:timestamp');
160
        Assert::positiveInteger($state['cmdotcom:timestamp']);
161
162
        $timestamp = $state['cmdotcom:timestamp'];
163
        $validUntil = $timestamp + $this->moduleConfig->getInteger('validUntil', 600);
164
165
        // Verify that code was entered within a reasonable amount of time
166
        if (time() > $validUntil) {
167
            $state['cmdotcom:expired'] = true;
168
169
            $id = Auth\State::saveState($state, 'codotcom:request');
0 ignored issues
show
Bug introduced by
It seems like $state can also be of type null; however, parameter $state of SimpleSAML\Auth\State::saveState() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

169
            $id = Auth\State::saveState(/** @scrutinizer ignore-type */ $state, 'codotcom:request');
Loading history...
170
            $url = Module::getModuleURL('cmdotcom/resendCode');
171
172
            return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
173
        }
174
175
        Assert::keyExists($state, 'cmdotcom:hash');
176
        Assert::stringNotEmpty($state['cmdotcom:hash']);
177
178
        $cryptoUtils = new Utils\Crypto();
179
        if ($cryptoUtils->pwValid($state['cmdotcom:hash'], $request->get('otp'))) {
0 ignored issues
show
Bug introduced by
It seems like $request->get('otp') can also be of type null; however, parameter $password of SimpleSAML\Utils\Crypto::pwValid() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

179
        if ($cryptoUtils->pwValid($state['cmdotcom:hash'], /** @scrutinizer ignore-type */ $request->get('otp'))) {
Loading history...
180
            // The user has entered the correct verification code
181
            return new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
182
        } else {
183
            $state['cmdotcom:invalid'] = true;
184
185
            $id = Auth\State::saveState($state, 'cmdotcom:request');
186
            $url = Module::getModuleURL('cmdotcom/enterCode');
187
188
            return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
189
        }
190
    }
191
192
193
    /**
194
     * Display the page where the user can trigger sending a new SMS.
195
     *
196
     * @return \SimpleSAML\XHTML\Template
197
     */
198
    public function promptResend(Request $request): Template
199
    {
200
        $id = $request->get('AuthState', null);
201
        if ($id === null) {
202
            throw new Error\BadRequest('Missing AuthState parameter.');
203
        }
204
205
        $state = $this->authState::loadState($id, 'cmdotcom:request');
206
207
        $t = new Template($this->config, 'cmdotcom:promptresend.twig');
208
        $t->data = [
209
            'AuthState' => $id,
210
        ];
211
212
        if (isset($state['cmdotcom:expired']) && ($state['cmdotcom:expired'] === true)) {
213
            $t->data['message'] = 'Your verification code has expired.';
214
        } elseif (isset($state['cmdotcom:sendFailure'])) {
215
            Assert::stringNotEmpty($state['cmdotcom:sendFailure']);
216
            $t->data['message'] = $state['cmdotcom:sendFailure'];
217
        } elseif (isset($state['cmdotcom:resendRequested']) && ($state['cmdotcom:resendRequested'] === true)) {
218
            $t->data['message'] = '';
219
        } else {
220
           throw new RuntimeException('Unknown request for SMS resend.');
221
        }
222
223
        return $t;
224
    }
225
226
227
    /**
228
     * Send an SMS and redirect to either the validation page or the resend-prompt
229
     *
230
     * @return \SimpleSAML\HTTP\RunnableResponse
231
     */
232
    public function sendCode(Request $request): RunnableResponse
233
    {
234
        $id = $request->get('AuthState', null);
235
        if ($id === null) {
236
            throw new Error\BadRequest('Missing AuthState parameter.');
237
        }
238
239
        $state = $this->authState::loadState($id, 'cmdotcom:request');
240
241
        // Generate the OTP
242
        $code = $this->randomUtils->generateOneTimePassword();
243
244
        Assert::digits($code, UnexpectedValueException::class);
245
        Assert::length($code, 6, UnexpectedValueException::class);
246
247
        $api_key = $this->moduleConfig->getString('api_key', null);
248
        Assert::notNull(
249
            $api_key,
250
            'Missing required REST API key for the cm.com service.',
251
            Error\ConfigurationError::class
252
        );
253
254
        Assert::keyExists($state, 'cmdotcom:recipient');
255
        Assert::keyExists($state, 'cmdotcom:originator');
256
257
        // Send SMS
258
        $response = $this->textUtils->sendMessage(
259
            $api_key,
260
            $code,
261
            $state['cmdotcom:recipient'],
262
            $state['cmdotcom:originator'],
263
        );
264
265
        if ($response->statusCode === TextClientStatusCodes::OK) {
266
            $this->logger::info("Message with ID " . $response->details[0]["reference"] . " was send successfully!");
267
268
            // Salt & hash it
269
            $cryptoUtils = new Utils\Crypto();
270
            $hash = $cryptoUtils->pwHash($code);
271
272
            // Store hash & time
273
            $state['cmdotcom:hash'] = $hash;
274
            $state['cmdotcom:timestamp'] = time();
275
276
            // Save state and redirect
277
            $id = Auth\State::saveState($state, 'cmdotcom:request');
0 ignored issues
show
Bug introduced by
It seems like $state can also be of type null; however, parameter $state of SimpleSAML\Auth\State::saveState() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

277
            $id = Auth\State::saveState(/** @scrutinizer ignore-type */ $state, 'cmdotcom:request');
Loading history...
278
            $url = Module::getModuleURL('cmdotcom/enterCode');
279
        } else {
280
            $msg = [
281
                "Message could not be send:",
282
                "Response: " . $response->statusMessage . " (" . $response->statusCode . ")"
283
            ];
284
285
            foreach ($msg as $line) {
286
                $this->logger::error($line);
287
            }
288
            $state['cmdotcom:sendFailure'] = $msg;
289
290
            // Save state and redirect
291
            $id = Auth\State::saveState($state, 'cmdotcom:request');
292
            $url = Module::getModuleURL('cmdotcom/promptResend');
293
        }
294
295
        return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
296
    }
297
}
298