OTP::validateCode()   B
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 55
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 34
nc 4
nop 1
dl 0
loc 55
rs 8.7537
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\cmdotcom\Controller;
6
7
use RuntimeException;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\{Auth, Configuration, Error, Logger, Module, Session, Utils};
10
use SimpleSAML\Module\cmdotcom\Utils\OTPClient;
11
use SimpleSAML\XHTML\Template;
12
use Symfony\Component\HttpFoundation\{RedirectResponse, Request, Response};
13
14
/**
15
 * Controller class for the cmdotcom module.
16
 *
17
 * This class serves the verification code and error views available in the module.
18
 *
19
 * @package SimpleSAML\Module\cmdotcom
20
 */
21
class OTP
22
{
23
    /** @var \SimpleSAML\Configuration */
24
    protected Configuration $config;
25
26
    /** @var \SimpleSAML\Logger */
27
    protected Logger $logger;
28
29
    /** @var \SimpleSAML\Session */
30
    protected Session $session;
31
32
    /** @var \SimpleSAML\Utils\HTTP */
33
    protected Utils\HTTP $httpUtils;
34
35
    /**
36
     * @var \SimpleSAML\Auth\State|string
37
     * @psalm-var \SimpleSAML\Auth\State|class-string
38
     */
39
    protected $authState = Auth\State::class;
40
41
42
    /**
43
     * OTP Controller constructor.
44
     *
45
     * @param \SimpleSAML\Configuration $config The configuration to use.
46
     * @param \SimpleSAML\Session $session The current user session.
47
     */
48
    public function __construct(Configuration $config, Session $session)
49
    {
50
        $this->config = $config;
51
        $this->httpUtils = new Utils\HTTP();
52
        $this->logger = new Logger();
53
        $this->session = $session;
54
    }
55
56
57
    /**
58
     * Inject the \SimpleSAML\Logger dependency.
59
     *
60
     * @param \SimpleSAML\Logger $logger
61
     */
62
    public function setLogger(Logger $logger): void
63
    {
64
        $this->logger = $logger;
65
    }
66
67
68
    /**
69
     * Inject the \SimpleSAML\Utils\HTTP dependency.
70
     *
71
     * @param \SimpleSAML\Utils\HTTP $httpUtils
72
     */
73
    public function setHttpUtils(Utils\HTTP $httpUtils): void
74
    {
75
        $this->httpUtils = $httpUtils;
76
    }
77
78
79
    /**
80
     * Inject the \SimpleSAML\Auth\State dependency.
81
     *
82
     * @param \SimpleSAML\Auth\State $authState
83
     */
84
    public function setAuthState(Auth\State $authState): void
85
    {
86
        $this->authState = $authState;
87
    }
88
89
90
    /**
91
     * Display the page where the validation code should be entered.
92
     *
93
     * @return \SimpleSAML\XHTML\Template
94
     */
95
    public function enterCode(Request $request): Template
96
    {
97
        $id = $request->query->get('AuthState', null);
98
        if ($id === null) {
99
            throw new Error\BadRequest('Missing AuthState parameter.');
100
        }
101
102
        $state = $this->authState::loadState($id, 'cmdotcom:request', false);
103
104
        $t = new Template($this->config, 'cmdotcom:entercode.twig');
105
        $t->data = [
106
            'AuthState' => $id,
107
        ];
108
109
        if (isset($state['cmdotcom:invalid']) && ($state['cmdotcom:invalid'] === true)) {
110
            $t->data['errorMessage'] = 'Code verification failed!';
111
        }
112
113
        return $t;
114
    }
115
116
117
    /**
118
     * Process the entered validation code.
119
     *
120
     * @return \Symfony\Component\HttpFoundation\Response|\Symfony\Component\HttpFoundation\RedirectResponse
121
     */
122
    public function validateCode(Request $request): Response|RedirectResponse
123
    {
124
        $id = $request->query->get('AuthState', null);
125
        if ($id === null) {
126
            throw new Error\BadRequest('Missing AuthState parameter.');
127
        }
128
129
        /** @var array $state */
130
        $state = $this->authState::loadState($id, 'cmdotcom:request', false);
131
132
        Assert::keyExists($state, 'cmdotcom:notBefore');
133
        $notBefore = strtotime($state['cmdotcom:notBefore']);
134
        Assert::positiveInteger($notBefore);
135
136
        Assert::keyExists($state, 'cmdotcom:notAfter');
137
        $notAfter = strtotime($state['cmdotcom:notAfter']);
138
        Assert::positiveInteger($notAfter);
139
140
        Assert::keyExists($state, 'cmdotcom:reference');
141
        $reference = $state['cmdotcom:reference'];
142
        Assert::uuid($reference);
143
144
        // Verify that code was entered within a reasonable amount of time
145
        if (time() < $notBefore || time() > $notAfter) {
146
            $state['cmdotcom:expired'] = true;
147
148
            $id = Auth\State::saveState($state, 'cmdotcom:request');
149
            $url = Module::getModuleURL('cmdotcom/promptResend');
150
151
            $this->logger::info("Code for message ID " . $reference . " has expired.");
152
            return $this->httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
153
        }
154
155
        $otpClient = new OTPClient($this->config);
156
        $response = $otpClient->verifyCode($state, $request->request->getAlnum('otp'));
157
        $responseMsg = json_decode((string) $response->getBody());
158
159
        if ($response->getStatusCode() === 200 && $responseMsg->valid === true) {
160
            // The user has entered the correct verification code
161
            $this->logger::info("Code for message ID " . $reference . " was verified successfully.");
162
163
            // Indicate that we are authenticated
164
            $session = Session::getSessionFromRequest();
165
            $session->setData('bool', 'cmdotcom:authenticated', true);
166
            $session->save();
167
168
            return Auth\ProcessingChain::class::resumeProcessing($state);
169
        } else {
170
            $this->logger::warning("Code for message ID " . $reference . " failed verification!");
171
            $state['cmdotcom:invalid'] = true;
172
173
            $id = Auth\State::saveState($state, 'cmdotcom:request');
174
            $url = Module::getModuleURL('cmdotcom/enterCode');
175
176
            return $this->httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
177
        }
178
    }
179
180
181
    /**
182
     * Display the page where the user can trigger sending a new SMS.
183
     *
184
     * @return \SimpleSAML\XHTML\Template
185
     */
186
    public function promptResend(Request $request): Template
187
    {
188
        $id = $request->query->get('AuthState', null);
189
        if ($id === null) {
190
            throw new Error\BadRequest('Missing AuthState parameter.');
191
        }
192
193
        /** @var array $state */
194
        $state = $this->authState::loadState($id, 'cmdotcom:request', false);
195
196
        $t = new Template($this->config, 'cmdotcom:promptresend.twig');
197
        $t->data = [
198
            'AuthState' => $id,
199
        ];
200
201
        if (isset($state['cmdotcom:expired']) && ($state['cmdotcom:expired'] === true)) {
202
            $t->data['message'] = ['Your verification code has expired.'];
203
        } elseif (isset($state['cmdotcom:sendFailure'])) {
204
            Assert::isArray($state['cmdotcom:sendFailure']);
205
            $t->data['message'] = $state['cmdotcom:sendFailure'];
206
        } else {
207
            throw new RuntimeException('Unknown request for SMS resend.');
208
        }
209
210
        return $t;
211
    }
212
213
214
    /**
215
     * Send an SMS and redirect to either the validation page or the resend-prompt
216
     *
217
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
218
     */
219
    public function sendCode(Request $request): RedirectResponse
220
    {
221
        $id = $request->query->get('AuthState', null);
222
        if ($id === null) {
223
            throw new Error\BadRequest('Missing AuthState parameter.');
224
        }
225
226
        /** @var array $state */
227
        $state = $this->authState::loadState($id, 'cmdotcom:request', false);
228
229
        $otpClient = new OTPClient($this->config);
230
        $response = $otpClient->sendCode($state);
231
        $responseMsg = json_decode((string) $response->getBody());
232
        if ($response->getStatusCode() === 200) {
233
            $this->logger::info("Message with ID " . $responseMsg->id . " was send successfully!");
234
235
            $state['cmdotcom:reference'] = $responseMsg->id;
236
            $state['cmdotcom:notBefore'] = $responseMsg->createdAt;
237
            $state['cmdotcom:notAfter'] = $responseMsg->expireAt;
238
239
            // Save state and redirect
240
            $id = Auth\State::saveState($state, 'cmdotcom:request');
241
            $url = Module::getModuleURL('cmdotcom/enterCode');
242
        } else {
243
            $msg = [
244
                sprintf(
245
                    "Message could not be send: HTTP/%d %s",
246
                    $response->getStatusCode(),
247
                    $response->getReasonPhrase(),
248
                ),
249
            ];
250
251
            if ($response->getStatusCode() < 500) {
252
                $msg[] = sprintf("Response: %s (%d)", $responseMsg->message, $responseMsg->status);
253
            }
254
255
            foreach ($msg as $line) {
256
                $this->logger::error($line);
257
            }
258
            $state['cmdotcom:sendFailure'] = $msg;
259
260
            // Save state and redirect
261
            $id = Auth\State::saveState($state, 'cmdotcom:request');
262
            $url = Module::getModuleURL('cmdotcom/promptResend');
263
        }
264
265
        return $this->httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
266
    }
267
}
268