Passed
Push — master ( 25a926...39145a )
by
unknown
20:30
created

TotpProvider::activate()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 36
rs 8.4444
cc 8
nc 8
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Authentication\Mfa\Provider;
19
20
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
21
use BaconQrCode\Renderer\ImageRenderer;
22
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
23
use BaconQrCode\Writer;
24
use Psr\Http\Message\ResponseInterface;
25
use Psr\Http\Message\ServerRequestInterface;
26
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderInterface;
27
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
28
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
29
use TYPO3\CMS\Core\Context\Context;
30
use TYPO3\CMS\Core\Http\HtmlResponse;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
33
use TYPO3\CMS\Fluid\View\StandaloneView;
34
35
/**
36
 * MFA provider for time-based one-time password authentication
37
 *
38
 * @internal should only be used by the TYPO3 Core
39
 */
40
class TotpProvider implements MfaProviderInterface
41
{
42
    private const MAX_ATTEMPTS = 3;
43
44
    protected Context $context;
45
46
    public function __construct(Context $context)
47
    {
48
        $this->context = $context;
49
    }
50
51
    /**
52
     * Check if a TOTP is given in the current request
53
     *
54
     * @param ServerRequestInterface $request
55
     * @return bool
56
     */
57
    public function canProcess(ServerRequestInterface $request): bool
58
    {
59
        return $this->getTotp($request) !== '';
60
    }
61
62
    /**
63
     * Evaluate if the provider is activated by checking
64
     * the active state from the provider properties.
65
     *
66
     * @param MfaProviderPropertyManager $propertyManager
67
     * @return bool
68
     */
69
    public function isActive(MfaProviderPropertyManager $propertyManager): bool
70
    {
71
        return (bool)$propertyManager->getProperty('active');
72
    }
73
74
    /**
75
     * Evaluate if the provider is temporarily locked by checking
76
     * the current attempts state from the provider properties.
77
     *
78
     * @param MfaProviderPropertyManager $propertyManager
79
     * @return bool
80
     */
81
    public function isLocked(MfaProviderPropertyManager $propertyManager): bool
82
    {
83
        $attempts = (int)$propertyManager->getProperty('attempts', 0);
84
85
        // Assume the provider is locked in case the maximum attempts are exceeded.
86
        // A provider however can only be locked if set up - an entry exists in database.
87
        return $propertyManager->hasProviderEntry() && $attempts >= self::MAX_ATTEMPTS;
88
    }
89
90
    /**
91
     * Verify the given TOTP and update the provider properties in case the TOTP is valid.
92
     *
93
     * @param ServerRequestInterface $request
94
     * @param MfaProviderPropertyManager $propertyManager
95
     * @return bool
96
     */
97
    public function verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
98
    {
99
        $totp = $this->getTotp($request);
100
        $secret = $propertyManager->getProperty('secret', '');
101
        $verified = GeneralUtility::makeInstance(Totp::class, $secret)->verifyTotp($totp, 2);
102
        if (!$verified) {
103
            $attempts = $propertyManager->getProperty('attempts', 0);
104
            $propertyManager->updateProperties(['attempts' => ++$attempts]);
105
            return false;
106
        }
107
        $propertyManager->updateProperties([
108
            'attempts' => 0,
109
            'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp')
110
        ]);
111
        return true;
112
    }
113
114
    /**
115
     * Activate the provider by checking the necessary parameters,
116
     * verifying the TOTP and storing the provider properties.
117
     *
118
     * @param ServerRequestInterface $request
119
     * @param MfaProviderPropertyManager $propertyManager
120
     * @return bool
121
     */
122
    public function activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
123
    {
124
        if ($this->isActive($propertyManager)) {
125
            // Return since the user already activated this provider
126
            return true;
127
        }
128
129
        if (!$this->canProcess($request)) {
130
            // Return since the request can not be processed by this provider
131
            return false;
132
        }
133
134
        $secret = (string)($request->getParsedBody()['secret'] ?? '');
135
        $checksum = (string)($request->getParsedBody()['checksum'] ?? '');
136
        if ($secret === '' || !hash_equals(GeneralUtility::hmac($secret, 'totp-setup'), $checksum)) {
137
            // Return since the request does not contain the initially created secret
138
            return false;
139
        }
140
141
        $totpInstance = GeneralUtility::makeInstance(Totp::class, $secret);
142
        if (!$totpInstance->verifyTotp($this->getTotp($request), 2)) {
143
            // Return since the given TOTP could not be verified
144
            return false;
145
        }
146
147
        // If valid, prepare the provider properties to be stored
148
        $properties = ['secret' => $secret, 'active' => true];
149
        if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') {
150
            $properties['name'] = $name;
151
        }
152
153
        // Usually there should be no entry if the provider is not activated, but to prevent the
154
        // provider from being unable to activate again, we update the existing entry in such case.
155
        return $propertyManager->hasProviderEntry()
156
            ? $propertyManager->updateProperties($properties)
157
            : $propertyManager->createProviderEntry($properties);
158
    }
159
160
    /**
161
     * Handle the save action by updating the provider properties
162
     *
163
     * @param ServerRequestInterface $request
164
     * @param MfaProviderPropertyManager $propertyManager
165
     * @return bool
166
     */
167
    public function update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
168
    {
169
        $name = (string)($request->getParsedBody()['name'] ?? '');
170
        if ($name !== '') {
171
            return $propertyManager->updateProperties(['name' => $name]);
172
        }
173
174
        // Provider properties successfully updated
175
        return true;
176
    }
177
178
    /**
179
     * Handle the unlock action by resetting the attempts provider property
180
     *
181
     * @param ServerRequestInterface $request
182
     * @param MfaProviderPropertyManager $propertyManager
183
     * @return bool
184
     */
185
    public function unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
186
    {
187
        if (!$this->isLocked($propertyManager)) {
188
            // Return since this provider is not locked
189
            return false;
190
        }
191
192
        // Reset the attempts
193
        return $propertyManager->updateProperties(['attempts' => 0]);
194
    }
195
196
    /**
197
     * Handle the deactivate action. For security reasons, the provider entry
198
     * is completely deleted and setting up this provider again, will therefore
199
     * create a brand new entry.
200
     *
201
     * @param ServerRequestInterface $request
202
     * @param MfaProviderPropertyManager $propertyManager
203
     * @return bool
204
     */
205
    public function deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
206
    {
207
        if (!$this->isActive($propertyManager)) {
208
            // Return since this provider is not activated
209
            return false;
210
        }
211
212
        // Delete the provider entry
213
        return $propertyManager->deleteProviderEntry();
214
    }
215
216
    /**
217
     * Initialize view and forward to the appropriate implementation
218
     * based on the view type to be returned.
219
     *
220
     * @param ServerRequestInterface $request
221
     * @param MfaProviderPropertyManager $propertyManager
222
     * @param string $type
223
     * @return ResponseInterface
224
     */
225
    public function handleRequest(
226
        ServerRequestInterface $request,
227
        MfaProviderPropertyManager $propertyManager,
228
        string $type
229
    ): ResponseInterface {
230
        $view = GeneralUtility::makeInstance(StandaloneView::class);
231
        $view->setTemplateRootPaths(['EXT:core/Resources/Private/Templates/Authentication/MfaProvider/Totp']);
232
        switch ($type) {
233
            case MfaViewType::SETUP:
234
                $this->prepareSetupView($view, $propertyManager);
235
                break;
236
            case MfaViewType::EDIT:
237
                $this->prepareEditView($view, $propertyManager);
238
                break;
239
            case MfaViewType::AUTH:
240
                $this->prepareAuthView($view, $propertyManager);
241
                break;
242
        }
243
        return new HtmlResponse($view->assign('providerIdentifier', $propertyManager->getIdentifier())->render());
244
    }
245
246
    /**
247
     * Generate a new shared secret, generate the otpauth URL and create a qr-code
248
     * for improved usability. Set template and assign necessary variables for the
249
     * setup view.
250
     *
251
     * @param ViewInterface $view
252
     * @param MfaProviderPropertyManager $propertyManager
253
     */
254
    protected function prepareSetupView(ViewInterface $view, MfaProviderPropertyManager $propertyManager): void
255
    {
256
        $userData = $propertyManager->getUser()->user ?? [];
257
        $secret = Totp::generateEncodedSecret([(string)($userData['uid'] ?? ''), (string)($userData['username'] ?? '')]);
258
        $totpInstance = GeneralUtility::makeInstance(Totp::class, $secret);
259
        $view->setTemplate('Setup');
0 ignored issues
show
Bug introduced by
The method setTemplate() does not exist on TYPO3\CMS\Extbase\Mvc\View\ViewInterface. It seems like you code against a sub-type of TYPO3\CMS\Extbase\Mvc\View\ViewInterface such as TYPO3\CMS\Extbase\Mvc\View\EmptyView or TYPO3\CMS\Fluid\View\AbstractTemplateView or TYPO3\CMS\Extbase\Mvc\View\NotFoundView. ( Ignorable by Annotation )

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

259
        $view->/** @scrutinizer ignore-call */ 
260
               setTemplate('Setup');
Loading history...
260
        $view->assignMultiple([
261
            'secret' => $secret,
262
            'qrCode' => $this->getSvgQrCode($totpInstance, $userData),
263
            // Generate hmac of the secret to prevent it from being changed in the setup from
264
            'checksum' => GeneralUtility::hmac($secret, 'totp-setup')
265
        ]);
266
    }
267
268
    /**
269
     * Set the template and assign necessary variables for the edit view
270
     *
271
     * @param ViewInterface $view
272
     * @param MfaProviderPropertyManager $propertyManager
273
     */
274
    protected function prepareEditView(ViewInterface $view, MfaProviderPropertyManager $propertyManager): void
275
    {
276
        $view->setTemplate('Edit');
277
        $view->assignMultiple([
278
            'name' => $propertyManager->getProperty('name'),
279
            'lastUsed' => $this->getDateTime($propertyManager->getProperty('lastUsed', 0)),
0 ignored issues
show
Bug introduced by
It seems like $propertyManager->getProperty('lastUsed', 0) can also be of type null; however, parameter $timestamp of TYPO3\CMS\Core\Authentic...Provider::getDateTime() does only seem to accept integer, 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

279
            'lastUsed' => $this->getDateTime(/** @scrutinizer ignore-type */ $propertyManager->getProperty('lastUsed', 0)),
Loading history...
280
            'updated' => $this->getDateTime($propertyManager->getProperty('updated', 0))
281
        ]);
282
    }
283
284
    /**
285
     * Set the template for the auth view where the user has to provide the TOTP
286
     *
287
     * @param ViewInterface $view
288
     * @param MfaProviderPropertyManager $propertyManager
289
     */
290
    protected function prepareAuthView(ViewInterface $view, MfaProviderPropertyManager $propertyManager): void
291
    {
292
        $view->setTemplate('Auth');
293
        $view->assign('isLocked', $this->isLocked($propertyManager));
294
    }
295
296
    /**
297
     * Internal helper method for fetching the TOTP from the request
298
     *
299
     * @param ServerRequestInterface $request
300
     * @return string
301
     */
302
    protected function getTotp(ServerRequestInterface $request): string
303
    {
304
        return trim((string)($request->getQueryParams()['totp'] ?? $request->getParsedBody()['totp'] ?? ''));
305
    }
306
307
    /**
308
     * Internal helper method for generating a svg QR-code for TOTP applications
309
     *
310
     * @param Totp $totp
311
     * @param array $userData
312
     * @return string
313
     */
314
    protected function getSvgQrCode(Totp $totp, array $userData): string
315
    {
316
        $qrCodeRenderer = new ImageRenderer(
317
            new RendererStyle(225, 4),
318
            new SvgImageBackEnd()
319
        );
320
321
        $content = $totp->getTotpAuthUrl(
322
            (string)($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? 'TYPO3'),
323
            (string)($userData['email'] ?? '') ?: (string)($userData['username'] ?? '')
324
        );
325
        return (new Writer($qrCodeRenderer))->writeString($content);
326
    }
327
328
    /**
329
     * Return the timestamp as local time (date string) by applying the globally configured format
330
     *
331
     * @param int $timestamp
332
     * @return string
333
     */
334
    protected function getDateTime(int $timestamp): string
335
    {
336
        if ($timestamp === 0) {
337
            return '';
338
        }
339
340
        return date(
341
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
342
            $timestamp
343
        ) ?: '';
344
    }
345
}
346