Completed
Push — master ( 0d1193...76ec59 )
by
unknown
15s
created

src/RegisterHandler.php (1 issue)

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