Passed
Push — master ( afc3b0...c561a7 )
by Tim
02:40
created

OTP::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 2
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', false);
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
        /** @var array $state */
127
        $state = $this->authState::loadState($id, 'cmdotcom:request', false);
128
129
        Assert::keyExists($state, 'cmdotcom:notBefore');
130
        $notBefore = strtotime($state['cmdotcom:notBefore']);
131
        Assert::positiveInteger($notBefore);
132
133
        Assert::keyExists($state, 'cmdotcom:notAfter');
134
        $notAfter = strtotime($state['cmdotcom:notAfter']);
135
        Assert::positiveInteger($notAfter);
136
137
        Assert::keyExists($state, 'cmdotcom:reference');
138
        $reference = $state['cmdotcom:reference'];
139
        Assert::uuid($reference);
140
141
        // Verify that code was entered within a reasonable amount of time
142
        if (time() < $notBefore || time() > $notAfter) {
143
            $state['cmdotcom:expired'] = true;
144
145
            $id = Auth\State::saveState($state, 'cmdotcom:request');
146
            $url = Module::getModuleURL('cmdotcom/promptResend');
147
148
            $this->logger::info("Code for message ID " . $reference . " has expired.");
149
            return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
150
        }
151
152
        $otpClient = new OTPClient($this->config);
153
        $response = $otpClient->verifyCode($state, $request->request->getAlnum('otp'));
154
        $responseMsg = json_decode((string) $response->getBody());
155
156
        if ($response->getStatusCode() === 200 && $responseMsg->valid === true) {
157
            // The user has entered the correct verification code
158
            $this->logger::info("Code for message ID " . $reference . " was verified successfully.");
159
            return new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
160
        } else {
161
            $this->logger::warning("Code for message ID " . $reference . " failed verification!");
162
            $state['cmdotcom:invalid'] = true;
163
164
            $id = Auth\State::saveState($state, 'cmdotcom:request');
165
            $url = Module::getModuleURL('cmdotcom/enterCode');
166
167
            return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
168
        }
169
    }
170
171
172
    /**
173
     * Display the page where the user can trigger sending a new SMS.
174
     *
175
     * @return \SimpleSAML\XHTML\Template
176
     */
177
    public function promptResend(Request $request): Template
178
    {
179
        $id = $request->query->get('AuthState', null);
180
        if ($id === null) {
181
            throw new Error\BadRequest('Missing AuthState parameter.');
182
        }
183
184
        /** @var array $state */
185
        $state = $this->authState::loadState($id, 'cmdotcom:request', false);
186
187
        $t = new Template($this->config, 'cmdotcom:promptresend.twig');
188
        $t->data = [
189
            'AuthState' => $id,
190
        ];
191
192
        if (isset($state['cmdotcom:expired']) && ($state['cmdotcom:expired'] === true)) {
193
            $t->data['message'] = ['Your verification code has expired.'];
194
        } elseif (isset($state['cmdotcom:sendFailure'])) {
195
            Assert::isArray($state['cmdotcom:sendFailure']);
196
            $t->data['message'] = $state['cmdotcom:sendFailure'];
197
        } else {
198
            throw new RuntimeException('Unknown request for SMS resend.');
199
        }
200
201
        return $t;
202
    }
203
204
205
    /**
206
     * Send an SMS and redirect to either the validation page or the resend-prompt
207
     *
208
     * @return \SimpleSAML\HTTP\RunnableResponse
209
     */
210
    public function sendCode(Request $request): RunnableResponse
211
    {
212
        $id = $request->query->get('AuthState', null);
213
        if ($id === null) {
214
            throw new Error\BadRequest('Missing AuthState parameter.');
215
        }
216
217
        /** @var array $state */
218
        $state = $this->authState::loadState($id, 'cmdotcom:request', false);
219
220
        $otpClient = new OTPClient($this->config);
221
        $response = $otpClient->sendCode($state);
222
        $responseMsg = json_decode((string) $response->getBody());
223
        if ($response->getStatusCode() === 200) {
224
            $this->logger::info("Message with ID " . $responseMsg->id . " was send successfully!");
225
226
            $state['cmdotcom:reference'] = $responseMsg->id;
227
            $state['cmdotcom:notBefore'] = $responseMsg->createdAt;
228
            $state['cmdotcom:notAfter'] = $responseMsg->expireAt;
229
230
            // Save state and redirect
231
            $id = Auth\State::saveState($state, 'cmdotcom:request');
232
            $url = Module::getModuleURL('cmdotcom/enterCode');
233
        } else {
234
            $msg = [
235
                sprintf(
236
                    "Message could not be send: HTTP/%d %s",
237
                    $response->getStatusCode(),
238
                    $response->getReasonPhrase()
239
                ),
240
                sprintf("Response: %s (%d)", $responseMsg->message, $responseMsg->status),
241
            ];
242
243
            foreach ($msg as $line) {
244
                $this->logger::error($line);
245
            }
246
            $state['cmdotcom:sendFailure'] = $msg;
247
248
            // Save state and redirect
249
            $id = Auth\State::saveState($state, 'cmdotcom:request');
250
            $url = Module::getModuleURL('cmdotcom/promptResend');
251
        }
252
253
        return new RunnableResponse([$this->httpUtils, 'redirectTrustedURL'], [$url, ['AuthState' => $id]]);
254
    }
255
}
256