Passed
Pull Request — master (#13)
by Simon
01:49
created

YubikeyMemberAuthenticator   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 25
dl 0
loc 227
rs 10
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getProvider() 0 3 1
A setProvider() 0 5 1
A supportedServices() 0 5 1
A get_name() 0 3 1
A __construct() 0 4 2
A updateMember() 0 12 3
B authenticateYubikey() 0 39 6
A authenticateNoYubikey() 0 10 2
A getLoginHandler() 0 3 1
B validateToken() 0 25 6
A checkYubikey() 0 12 1
1
<?php
2
3
namespace Firesphere\YubiAuth\Authenticators;
4
5
use Exception;
6
use Firesphere\BootstrapMFA\Authenticators\BootstrapMFAAuthenticator;
7
use Firesphere\BootstrapMFA\Handlers\BootstrapMFALoginHandler;
0 ignored issues
show
Bug introduced by
The type Firesphere\BootstrapMFA\...ootstrapMFALoginHandler was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Firesphere\YubiAuth\Handlers\YubikeyLoginHandler;
9
use Firesphere\YubiAuth\Helpers\QwertyConvertor;
10
use Firesphere\YubiAuth\Providers\YubikeyAuthProvider;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Core\Environment;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\ORM\ValidationException;
16
use SilverStripe\ORM\ValidationResult;
17
use SilverStripe\Security\Authenticator;
18
use SilverStripe\Security\Member;
19
use SilverStripe\Security\PasswordEncryptor_NotFoundException;
20
use Yubikey\Response;
21
use Yubikey\Validate;
22
23
/**
24
 * Class YubikeyAuthenticator
25
 *
26
 * Enable Yubikey Authentication for SilverStripe CMS and member-protected pages.
27
 */
28
class YubikeyMemberAuthenticator extends BootstrapMFAAuthenticator
29
{
30
31
    /**
32
     * @var Validate
33
     */
34
    protected $yubiService;
35
36
    /**
37
     * @var YubikeyAuthProvider
38
     */
39
    protected $provider;
40
41
    /**
42
     * @var string
43
     */
44
    private $authenticatorName = 'yubiauth';
0 ignored issues
show
introduced by
The private property $authenticatorName is not used, and could be removed.
Loading history...
45
46
    /**
47
     * Set the provider to a YubikeyAuthProvider instance
48
     *
49
     * YubikeyMemberAuthenticator constructor.
50
     */
51
    public function __construct()
52
    {
53
        if (!$this->provider) {
54
            $this->provider = Injector::inst()->get(YubikeyAuthProvider::class);
55
        }
56
    }
57
58
    /**
59
     * Name of this authenticator
60
     *
61
     * @return string
62
     */
63
    public static function get_name()
64
    {
65
        return _t('YubikeyAuthenticator.TITLE', 'Yubikey 2 factor login');
66
    }
67
68
    /**
69
     * @return YubikeyAuthProvider
70
     */
71
    public function getProvider()
72
    {
73
        return $this->provider;
74
    }
75
76
    /**
77
     * @param YubikeyAuthProvider $provider
78
     * @return $this
79
     */
80
    public function setProvider($provider)
81
    {
82
        $this->provider = $provider;
83
84
        return $this;
85
    }
86
87
    public function supportedServices()
88
    {
89
        // Bitwise-OR of all the supported services in this Authenticator, to make a bitmask
90
        return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD
91
            | Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD;
92
    }
93
94
    /**
95
     * @inheritdoc
96
     *
97
     * @param array $data
98
     * @param HTTPRequest $request
99
     * @param ValidationResult $validationResult
100
     *
101
     * @return ValidationResult|Member
102
     * @throws ValidationException
103
     * @throws PasswordEncryptor_NotFoundException
104
     */
105
    public function validateToken($data, $request, &$validationResult = null)
106
    {
107
        if (!$validationResult instanceof ValidationResult) {
108
            $validationResult = ValidationResult::create();
109
        }
110
111
        $memberID = $request->getSession()->get(BootstrapMFALoginHandler::SESSION_KEY . '.MemberID');
112
        // First, let's see if we know the member
113
        /** @var Member|null $member */
114
        $member = Member::get()->filter(['ID' => $memberID])->first();
115
116
        // Continue if we have a valid member
117
        if ($member && $member instanceof Member) {
118
119
            // We do not have to check the YubiAuth for now.
120
            if (!$member->MFAEnabled && empty($data['yubiauth'])) {
121
                return $this->authenticateNoYubikey($member);
122
            }
123
124
            // If we know the member, and it's YubiAuth enabled, continue.
125
            return $this->checkYubikey($data, $member);
126
        }
127
128
        $validationResult->addError(_t(__CLASS__ . '.MEMBERNOTFOUND', 'Could not identify member'));
129
        return $validationResult;
130
    }
131
132
    /**
133
     * Handle login if the user did not enter a Yubikey string.
134
     * Will break out and return NULL if the member should use their Yubikey
135
     *
136
     * @param  Member $member
137
     * @return ValidationResult|Member
138
     * @throws ValidationException
139
     */
140
    private function authenticateNoYubikey($member)
141
    {
142
        ++$member->NoYubikeyCount;
143
        $member->write();
144
        $yubiAuthNoYubi = $this->provider->checkNoYubiAttempts($member);
145
        if ($yubiAuthNoYubi instanceof ValidationResult) {
146
            return $yubiAuthNoYubi;
147
        }
148
149
        return $member;
150
    }
151
152
    /**
153
     * @param $data
154
     * @param $member
155
     * @return ValidationResult|Member
156
     * @throws ValidationException
157
     * @throws PasswordEncryptor_NotFoundException
158
     */
159
    protected function checkYubikey($data, $member)
160
    {
161
        /** @var Validate $service */
162
        $this->yubiService = Injector::inst()->createWithArgs(
163
            Validate::class,
164
            [
165
                Environment::getEnv('YUBIAUTH_APIKEY'),
166
                Environment::getEnv('YUBIAUTH_CLIENTID'),
167
            ]
168
        );
169
170
        return $this->authenticateYubikey($data, $member);
171
    }
172
173
    /**
174
     * Validate a member plus it's yubikey login. It compares the fingerprintt and after that,
175
     * tries to validate the Yubikey string
176
     *
177
     * @param  array $data
178
     * @param  Member $member
179
     * @return ValidationResult|Member
180
     * @throws ValidationException
181
     * @throws PasswordEncryptor_NotFoundException
182
     */
183
    private function authenticateYubikey($data, $member)
184
    {
185
        if ($url = Config::inst()->get(self::class, 'AuthURL')) {
186
            $this->yubiService->setHost($url);
187
        }
188
        $yubiCode = QwertyConvertor::convertString($data['yubiauth']);
189
        $yubiFingerprint = substr($yubiCode, 0, -32);
190
        $validationResult = ValidationResult::create();
191
192
        if ($member->Yubikey) {
193
            $validationResult = $this->provider->validateToken($member, $yubiFingerprint);
194
            if (!$validationResult->isValid()) {
195
                $member->registerFailedLogin();
196
197
                return $validationResult;
198
            }
199
        }
200
        try {
201
            /** @var Response $result */
202
            $result = $this->yubiService->check($yubiCode);
203
            $this->updateMember($member, $yubiFingerprint);
204
        } catch (Exception $e) {
205
            $validationResult->addError($e->getMessage());
206
207
            $member->registerFailedLogin();
208
209
            return $validationResult;
210
        }
211
        if ($result->success() === true) {
212
            $this->updateMember($member, $yubiFingerprint);
213
214
            return $member;
215
        }
216
217
        $validationResult = ValidationResult::create();
218
        $validationResult->addError(_t('YubikeyAuthenticator.ERROR', 'Yubikey authentication error'));
219
        $member->registerFailedLogin();
220
221
        return $validationResult;
222
    }
223
224
    /**
225
     * Update the member to forcefully enable YubiAuth
226
     * Also, register the Yubikey to the member.
227
     * Documentation:
228
     * https://developers.yubico.com/yubikey-val/Getting_Started_Writing_Clients.html
229
     *
230
     * @param Member $member
231
     * @param string $yubiString The Identifier String of the Yubikey
232
     * @throws ValidationException
233
     */
234
    private function updateMember($member, $yubiString)
235
    {
236
        $member->registerSuccessfulLogin();
237
        $member->NoYubikeyCount = 0;
238
239
        if (!$member->MFAEnabled) {
240
            $member->MFAEnabled = true;
241
        }
242
        if (!$member->Yubikey) {
243
            $member->Yubikey = $yubiString;
244
        }
245
        $member->write();
246
    }
247
248
    /**
249
     * @param string $link
250
     * @return \SilverStripe\Security\MemberAuthenticator\LoginHandler|static
251
     */
252
    public function getLoginHandler($link)
253
    {
254
        return YubikeyLoginHandler::create($link, $this);
255
    }
256
}
257