Issues (25)

src/Extensions/MemberExtension.php (4 issues)

1
<?php
2
3
namespace Firesphere\BootstrapMFA\Extensions;
4
5
use DateTime;
6
use Firesphere\BootstrapMFA\Authenticators\BootstrapMFAAuthenticator;
7
use Firesphere\BootstrapMFA\Models\BackupCode;
8
use Firesphere\BootstrapMFA\Providers\BootstrapMFAProvider;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Forms\CheckboxField;
13
use SilverStripe\Forms\FieldList;
14
use SilverStripe\Forms\LiteralField;
15
use SilverStripe\Forms\Tab;
16
use SilverStripe\ORM\DataExtension;
17
use SilverStripe\ORM\DataList;
18
use SilverStripe\ORM\FieldType\DBDatetime;
19
use SilverStripe\Security\Member;
20
use SilverStripe\Security\PasswordEncryptor;
21
use SilverStripe\Security\Security;
22
use SilverStripe\SiteConfig\SiteConfig;
23
24
/**
25
 * Class MemberExtension
26
 *
27
 * @package Firesphere\BootstrapMFA
28
 * @property Member|MemberExtension $owner
29
 * @property boolean $MFAEnabled
30
 * @property string $PrimaryMFA
31
 * @property string $BackupCodeSalt
32
 * @method DataList|BackupCode[] BackupCodes()
33
 */
34
class MemberExtension extends DataExtension
35
{
36
    /**
37
     * @var array
38
     */
39
    private static $db = [
0 ignored issues
show
The private property $db is not used, and could be removed.
Loading history...
40
        'MFAEnabled'     => 'Boolean(false)',
41
        'PrimaryMFA'     => 'Varchar(255)',
42
        'BackupCodeSalt' => 'Varchar(255)'
43
    ];
44
45
    /**
46
     * @var array
47
     */
48
    private static $has_many = [
0 ignored issues
show
The private property $has_many is not used, and could be removed.
Loading history...
49
        'BackupCodes' => BackupCode::class
50
    ];
51
52
    /**
53
     * @var bool
54
     */
55
    public $updateMFA = false;
56
57
    /**
58
     * @param FieldList $fields
59
     */
60
    public function updateCMSFields(FieldList $fields)
61
    {
62
        // Force the updateMFA value of this field. This resolves that when it's checked and submitted
63
        // The checkbox stays checked.
64
        $this->updateMFA = false;
65
        $fields->removeByName(['BackupCodes', 'PrimaryMFA', 'BackupCodeSalt']);
66
        $session = Controller::curr()->getRequest()->getSession();
67
        $rootTabSet = $fields->fieldByName('Root');
68
        // We need to push the tab for unit tests
69
        $tab = Tab::create(
70
            'MFA',
71
            _t(self::class . '.MFATAB', 'Multi Factor Authentication')
72
        );
73
        $rootTabSet->push(
74
            $tab
75
        );
76
        $fields->addFieldToTab(
77
            'Root.MFA',
78
            $enabled = CheckboxField::create('MFAEnabled', _t(self::class . '.MFAEnabled', 'MFA Enabled'))
79
        );
80
        $fields->addFieldToTab(
81
            'Root.MFA',
82
            CheckboxField::create('updateMFA', _t(self::class . '.RESETMFA', 'Reset MFA codes'))
83
        );
84
85
        if ($session->get('tokens')) {
86
            $field = LiteralField::create('tokens', $session->get('tokens'));
87
            $fields->addFieldToTab('Root.MFA', $field);
88
            $session->clear('tokens');
89
        }
90
    }
91
92
    /**
93
     * Force enable MFA on the member if needed
94
     */
95
    public function onBeforeWrite()
96
    {
97
        if (!$this->owner->MFAEnabled && SiteConfig::current_site_config()->ForceMFA) {
98
            $this->owner->MFAEnabled = true;
99
            $this->owner->updateMFA = true;
100
        }
101
        if (!$this->owner->BackupCodeSalt || $this->owner->updateMFA) {
102
            $algorithm = Security::config()->get('password_encryption_algorithm');
103
104
            $encryptor = PasswordEncryptor::create_for_algorithm($algorithm);
105
106
            // No password. It's not even used in the salt generation
107
            $this->owner->BackupCodeSalt = $encryptor->salt('');
108
        }
109
    }
110
111
    /**
112
     *
113
     * @throws \Psr\Container\NotFoundExceptionInterface
114
     * @throws \SilverStripe\ORM\ValidationException
115
     */
116
    public function onAfterWrite()
117
    {
118
        parent::onAfterWrite();
119
        if ($this->owner->updateMFA) {
120
            /** @var BootstrapMFAProvider $provider */
121
            $provider = Injector::inst()->get(BootstrapMFAProvider::class);
122
            $provider->setMember($this->owner);
0 ignored issues
show
It seems like $this->owner can also be of type Firesphere\BootstrapMFA\Extensions\MemberExtension; however, parameter $member of Firesphere\BootstrapMFA\...FAProvider::setMember() does only seem to accept SilverStripe\Security\Member, 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

122
            $provider->setMember(/** @scrutinizer ignore-type */ $this->owner);
Loading history...
123
            $provider->updateTokens();
124
        }
125
    }
126
127
    /**
128
     * Check if a member is in grace period based on Created, date or enforcement
129
     * @return bool
130
     * @throws \Exception
131
     */
132
    public function isInGracePeriod()
133
    {
134
        /** @var Member|MemberExtension $member */
135
        $member = $this->owner;
136
137
        // If MFA is enabled on the member, we're always using it
138
        if ($member->MFAEnabled) {
139
            return false;
140
        }
141
142
        /** @var SiteConfig|SiteConfigExtension $config */
143
        $config = SiteConfig::current_site_config();
144
        // If MFA is not enforced, we're in an endless grace period
145
        if ($config->ForceMFA === null) {
146
            return true;
147
        }
148
149
        // Default the grace start day
150
        $graceStartDay = ($member->Created > $config->ForceMFA) ? $member->Created : $config->ForceMFA;
0 ignored issues
show
Bug Best Practice introduced by
The property Created does not exist on Firesphere\BootstrapMFA\Extensions\MemberExtension. Did you maybe forget to declare it?
Loading history...
151
        $graceStartDay = new DateTime($graceStartDay);
152
153
        $gracePeriodInDays = Config::inst()->get(BootstrapMFAAuthenticator::class, 'grace_period');
154
155
        $nowDate = new DateTime(DBDatetime::now()->Format(DBDatetime::ISO_DATE));
156
157
        $daysSinceGraceStart = $nowDate->diff($graceStartDay)->days;
158
159
        return $daysSinceGraceStart < $gracePeriodInDays;
160
    }
161
}
162