RegisterHandler::start()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 3
eloc 14
c 4
b 0
f 0
nc 2
nop 1
dl 0
loc 22
rs 9.7998
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\TOTP;
6
7
use ParagonIE\ConstantTime\Base32;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Core\Config\Configurable;
10
use SilverStripe\Core\Environment;
11
use SilverStripe\Core\Extensible;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\MFA\Exception\AuthenticationFailedException;
14
use SilverStripe\MFA\Method\Handler\RegisterHandlerInterface;
15
use SilverStripe\MFA\Service\EncryptionAdapterInterface;
16
use SilverStripe\MFA\State\Result;
17
use SilverStripe\MFA\Store\StoreInterface;
18
use SilverStripe\Security\Member;
19
use SilverStripe\Security\Security;
20
use SilverStripe\SiteConfig\SiteConfig;
21
22
/**
23
 * Handles registration requests using a time-based one-time password (TOTP) with the silverstripe/mfa module.
24
 */
25
class RegisterHandler implements RegisterHandlerInterface
26
{
27
    use Configurable;
28
    use Extensible;
29
    use TOTPAware;
30
31
    /**
32
     * The link to SilverStripe user help documentation for this authenticator.
33
     *
34
     * @config
35
     * @var string
36
     */
37
    private static $user_help_link = 'https://userhelp.silverstripe.org/en/4/optional_features/multi-factor_authentication/user_manual/using_authenticator_apps/'; // phpcs:ignore
38
39
    /**
40
     * The desired length of the TOTP secret. This affects the UI, since it is displayed to the user to be entered
41
     * manually if they cannot scan the QR code.
42
     *
43
     * @config
44
     * @var int
45
     */
46
    private static $secret_length = 16;
47
48
    public function start(StoreInterface $store): array
49
    {
50
        $store->setState([
51
            'secret' => $this->generateSecret(),
52
        ]);
53
54
        $totp = $this->getTotp($store);
55
56
        $member = $store->getMember() ?: Security::getCurrentUser();
57
        if ($member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
58
            $uniqueIdentifier = (string) Member::config()->get('unique_identifier_field');
59
            $totp->setLabel($member->{$uniqueIdentifier});
60
        }
61
        $totp->setIssuer(SiteConfig::current_site_config()->Title);
62
63
        $this->extend('updateTotp', $totp, $member);
64
65
        return [
66
            'enabled' => !empty(Environment::getEnv('SS_MFA_SECRET_KEY')),
67
            'uri' => $totp->getProvisioningUri(),
68
            'code' => $totp->getSecret(),
69
            'codeLength' => Injector::inst()->create(Method::class)->getCodeLength(),
70
        ];
71
    }
72
73
    /**
74
     * Generates a TOTP secret to the configured maximum length
75
     *
76
     * @return string
77
     */
78
    protected function generateSecret(): string
79
    {
80
        $length = $this->config()->get('secret_length');
81
        return substr(trim(Base32::encodeUpper(random_bytes(64)), '='), 0, $length);
82
    }
83
84
    /**
85
     * Validate the provided TOTP code and return the TOTP secret to be stored against the RegisteredMethod model.
86
     * Will throw an exception if the code is invalid.
87
     *
88
     * @param HTTPRequest $request
89
     * @param StoreInterface $store
90
     * @return Result
91
     * @throws AuthenticationFailedException
92
     */
93
    public function register(HTTPRequest $request, StoreInterface $store): Result
94
    {
95
        $data = json_decode($request->getBody(), true);
96
        $result = $this->getTotp($store)->verify($data['code'] ?? '');
97
        if (!$result) {
98
            return Result::create(false, _t(__CLASS__ . '.INVALID_CODE', 'Provided code was not valid'));
99
        }
100
101
        $key = $this->getEncryptionKey();
102
        if (empty($key)) {
103
            throw new AuthenticationFailedException(
104
                'Please define a SS_MFA_SECRET_KEY environment variable for encryption'
105
            );
106
        }
107
108
        // Encrypt the TOTP secret before storing it
109
        $secret = Injector::inst()->get(EncryptionAdapterInterface::class)->encrypt(
110
            $store->getState()['secret'],
111
            $key
112
        );
113
114
        return Result::create()->setContext(['secret' => $secret]);
115
    }
116
117
    public function getDescription(): string
118
    {
119
        return _t(
120
            __CLASS__ . '.DESCRIPTION',
121
            'Use an authentication app such as Google Authenticator to scan the following code'
122
        );
123
    }
124
125
    public function getSupportLink(): string
126
    {
127
        return (string) $this->config()->get('user_help_link');
128
    }
129
130
    public function getSupportText(): string
131
    {
132
        return _t(__CLASS__ . '.SUPPORT_LINK_DESCRIPTION', 'How to use authenticator apps.');
133
    }
134
135
    public function getComponent(): string
136
    {
137
        return 'TOTPRegister';
138
    }
139
}
140