Passed
Push — master ( b215e3...34e661 )
by Tim
11:08
created

OTP::validateCode()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 27
c 4
b 0
f 0
dl 0
loc 43
rs 8.8657
cc 6
nc 4
nop 1
1
<?php
2
3
namespace SimpleSAML\Module\cmdotcom\Controller;
4
5
use GuzzleHttp\Client as GuzzleClient;
6
use RuntimeException;
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\{Auth, Configuration, Error, Logger, Module, Session, Utils};
9
use SimpleSAML\HTTP\RunnableResponse;
10
use SimpleSAML\Module\cmdotcom\Utils\OTPClient;
11
use SimpleSAML\XHTML\Template;
12
use Symfony\Component\HttpFoundation\{RedirectResponse, Request};
13
use UnexpectedValueException;
14
15
/**
16
 * Controller class for the cmdotcom module.
17
 *
18
 * This class serves the verification code and error views available in the module.
19
 *
20
 * @package SimpleSAML\Module\cmdotcom
21
 */
22
class OTP
23
{
24
    /** @var \SimpleSAML\Configuration */
25
    protected Configuration $config;
26
27
    /** @var \SimpleSAML\Logger */
28
    protected Logger $logger;
29
30
    /** @var \SimpleSAML\Session */
31
    protected Session $session;
32
33
    /** @var \SimpleSAML\Utils\HTTP */
34
    protected Utils\HTTP $httpUtils;
35
36
    /**
37
     * @var \SimpleSAML\Auth\State|string
38
     * @psalm-var \SimpleSAML\Auth\State|class-string
39
     */
40
    protected $authState = Auth\State::class;
41
42
43
    /**
44
     * OTP Controller constructor.
45
     *
46
     * @param \SimpleSAML\Configuration $config The configuration to use.
47
     * @param \SimpleSAML\Session $session The current user session.
48
     */
49
    public function __construct(Configuration $config, Session $session)
50
    {
51
        $this->config = $config;
52
        $this->httpUtils = new Utils\HTTP();
53
        $this->logger = new Logger();
54
        $this->session = $session;
55
    }
56
57
58
    /**
59
     * Inject the \SimpleSAML\Logger dependency.
60
     *
61
     * @param \SimpleSAML\Logger $logger
62
     */
63
    public function setLogger(Logger $logger): void
64
    {
65
        $this->logger = $logger;
66
    }
67
68
69
    /**
70
     * Inject the \SimpleSAML\Utils\HTTP dependency.
71
     *
72
     * @param \SimpleSAML\Utils\HTTP $httpUtils
73
     */
74
    public function setHttpUtils(Utils\HTTP $httpUtils): void
75
    {
76
        $this->httpUtils = $httpUtils;
77
    }
78
79
80
    /**
81
     * Inject the \SimpleSAML\Auth\State dependency.
82
     *
83
     * @param \SimpleSAML\Auth\State $authState
84
     */
85
    public function setAuthState(Auth\State $authState): void
86
    {
87
        $this->authState = $authState;
88
    }
89
90
91
    /**
92
     * Display the page where the validation code should be entered.
93
     *
94
     * @return \SimpleSAML\XHTML\Template
95
     */
96
    public function enterCode(Request $request): Template
97
    {
98
        $id = $request->query->get('AuthState', null);
99
        if ($id === null) {
100
            throw new Error\BadRequest('Missing AuthState parameter.');
101
        }
102
103
        $this->authState::loadState($id, 'cmdotcom:request');
104
105
        $t = new Template($this->config, 'cmdotcom:entercode.twig');
106
        $t->data = [
107
            'AuthState' => $id,
108
        ];
109
110
        return $t;
111
    }
112
113
114
    /**
115
     * Process the entered validation code.
116
     *
117
     * @return \SimpleSAML\HTTP\RunnableResponse
118
     */
119
    public function validateCode(Request $request): RunnableResponse
120
    {
121
        $id = $request->query->get('AuthState', null);
122
        if ($id === null) {
123
            throw new Error\BadRequest('Missing AuthState parameter.');
124
        }
125
126
        $state = $this->authState::loadState($id, 'cmdotcom:request');
127
128
        Assert::keyExists($state, 'cmdotcom:notBefore');
129
        $notBefore = strtotime($state['cmdotcom:notBefore']);
130
        Assert::positiveInteger($notBefore);
131
132
        Assert::keyExists($state, 'cmdotcom:notAfter');
133
        $notAfter = strtotime($state['cmdotcom:notAfter']);
134
        Assert::positiveInteger($notAfter);
135
136
        // Verify that code was entered within a reasonable amount of time
137
        if (time() < $notBefore || time() > $notAfter) {
138
            $state['cmdotcom:expired'] = true;
139
140
            $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

140
            $id = Auth\State::saveState(/** @scrutinizer ignore-type */ $state, 'cmdotcom:request');
Loading history...
141
            $url = Module::getModuleURL('cmdotcom/promptResend');
142
143
            return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
144
        }
145
146
        $otpClient = new OTPClient($this->config);
147
        $response = $otpClient->verifyCode($state, $request->request->get('otp'));
0 ignored issues
show
Bug introduced by
It seems like $state can also be of type null; however, parameter $state of SimpleSAML\Module\cmdotc...OTPClient::verifyCode() 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

147
        $response = $otpClient->verifyCode(/** @scrutinizer ignore-type */ $state, $request->request->get('otp'));
Loading history...
148
        $responseMsg = json_decode((string) $response->getBody());
149
150
        if ($response->getStatusCode() === 200 && $responseMsg->valid === true) {
151
            // The user has entered the correct verification code
152
            $this->logger::info("Code for message ID " . $responseMsg->id . " was verified successfully.");
153
            return new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
154
        } else {
155
            $this->logger::warning("Code for message ID " . $state['cmdotcom:reference'] . " failed verification!");
156
            $state['cmdotcom:invalid'] = true;
157
158
            $id = Auth\State::saveState($state, 'cmdotcom:request');
159
            $url = Module::getModuleURL('cmdotcom/enterCode');
160
161
            return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
162
        }
163
    }
164
165
166
    /**
167
     * Display the page where the user can trigger sending a new SMS.
168
     *
169
     * @return \SimpleSAML\XHTML\Template
170
     */
171
    public function promptResend(Request $request): Template
172
    {
173
        $id = $request->query->get('AuthState', null);
174
        if ($id === null) {
175
            throw new Error\BadRequest('Missing AuthState parameter.');
176
        }
177
178
        $state = $this->authState::loadState($id, 'cmdotcom:request');
179
180
        $t = new Template($this->config, 'cmdotcom:promptresend.twig');
181
        $t->data = [
182
            'AuthState' => $id,
183
        ];
184
185
        if (isset($state['cmdotcom:expired']) && ($state['cmdotcom:expired'] === true)) {
186
            $t->data['message'] = 'Your verification code has expired.';
187
        } elseif (isset($state['cmdotcom:sendFailure'])) {
188
            Assert::isArray($state['cmdotcom:sendFailure']);
189
            $t->data['message'] = $state['cmdotcom:sendFailure'];
190
        } elseif (isset($state['cmdotcom:resendRequested']) && ($state['cmdotcom:resendRequested'] === true)) {
191
            $t->data['message'] = [];
192
        } else {
193
            throw new RuntimeException('Unknown request for SMS resend.');
194
        }
195
196
        return $t;
197
    }
198
199
200
    /**
201
     * Send an SMS and redirect to either the validation page or the resend-prompt
202
     *
203
     * @return \SimpleSAML\HTTP\RunnableResponse
204
     */
205
    public function sendCode(Request $request): RunnableResponse
206
    {
207
        $id = $request->query->get('AuthState', null);
208
        if ($id === null) {
209
            throw new Error\BadRequest('Missing AuthState parameter.');
210
        }
211
212
        $state = $this->authState::loadState($id, 'cmdotcom:request');
213
214
        $otpClient = new OTPClient($this->config);
215
        $response = $otpClient->sendCode($state);
0 ignored issues
show
Bug introduced by
It seems like $state can also be of type null; however, parameter $state of SimpleSAML\Module\cmdotc...s\OTPClient::sendCode() 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

215
        $response = $otpClient->sendCode(/** @scrutinizer ignore-type */ $state);
Loading history...
216
        $responseMsg = json_decode((string) $response->getBody());
217
        if ($response->getStatusCode() === 200) {
218
            $this->logger::info("Message with ID " . $responseMsg->id . " was send successfully!");
219
220
            $state['cmdotcom:reference'] = $responseMsg->id;
221
            $state['cmdotcom:notBefore'] = $responseMsg->createdAt;
222
            $state['cmdotcom:notAfter'] = $responseMsg->expireAt;
223
224
            // Save state and redirect
225
            $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

225
            $id = Auth\State::saveState(/** @scrutinizer ignore-type */ $state, 'cmdotcom:request');
Loading history...
226
            $url = Module::getModuleURL('cmdotcom/enterCode');
227
        } else {
228
            $msg = [
229
                sprintf(
230
                    "Message could not be send: HTTP/%d %s",
231
                    $response->getStatusCode(),
232
                    $response->getReasonPhrase()
233
                ),
234
                sprintf("Response: %s (%d)", $responseMsg->message, $responseMsg->status),
235
            ];
236
237
            foreach ($msg as $line) {
238
                $this->logger::error($line);
239
            }
240
            $state['cmdotcom:sendFailure'] = $msg;
241
242
            // Save state and redirect
243
            $id = Auth\State::saveState($state, 'cmdotcom:request');
244
            $url = Module::getModuleURL('cmdotcom/promptResend');
245
        }
246
247
        return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
248
    }
249
}
250