YubikeyAuthProvider   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 270
rs 10
c 0
b 0
f 0
wmc 30

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 2
A checkNoYubiAttempts() 0 8 2
A checkNoYubiLogins() 0 18 3
A checkNoYubiDays() 0 22 3
A checkYubikey() 0 3 1
A updateMember() 0 12 3
A getService() 0 3 1
A validateMemberID() 0 5 3
A validateFingerprint() 0 8 3
A setService() 0 3 1
B authenticateYubikey() 0 35 5
A validateMemberCount() 0 13 2
A validateToken() 0 11 1
1
<?php
2
3
namespace Firesphere\YubiAuth\Providers;
4
5
use DateTime;
6
use Exception;
7
use Firesphere\BootstrapMFA\Providers\BootstrapMFAProvider;
8
use Firesphere\BootstrapMFA\Providers\MFAProvider;
9
use Firesphere\YubiAuth\Helpers\QwertyConvertor;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Config\Configurable;
12
use SilverStripe\Core\Environment;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\ORM\DataList;
15
use SilverStripe\ORM\ValidationException;
16
use SilverStripe\ORM\ValidationResult;
17
use SilverStripe\Security\Member;
18
use Yubikey\Response;
19
use Yubikey\Validate;
20
21
/**
22
 * Class YubikeyAuthProvider
23
 *
24
 * @package Firesphere\YubiAuth
25
 */
26
class YubikeyAuthProvider extends BootstrapMFAProvider implements MFAProvider
27
{
28
    use Configurable;
29
30
    /**
31
     * @var Validate
32
     */
33
    protected $service;
34
35
    /**
36
     * Setup
37
     */
38
    public function __construct()
39
    {
40
        /** @var Validate $service */
41
        $this->service = Injector::inst()->createWithArgs(
42
            Validate::class,
43
            [
44
                Environment::getEnv('YUBIAUTH_APIKEY'),
45
                Environment::getEnv('YUBIAUTH_CLIENTID'),
46
            ]
47
        );
48
49
        if ($url = Config::inst()->get(static::class, 'AuthURL')) {
50
            $url = (array)$url;
51
            $this->service->setHosts($url);
52
        }
53
    }
54
55
    /**
56
     * @param Member $member
57
     * @return ValidationResult|Member
58
     */
59
    public function checkNoYubiAttempts(Member $member)
60
    {
61
        $noYubiLogins = $this->checkNoYubiLogins($member);
62
        if ($noYubiLogins instanceof Member) {
63
            return $this->checkNoYubiDays($member);
64
        }
65
66
        return $noYubiLogins;
67
    }
68
69
    /**
70
     * Check if a member is allowed to login without a yubikey
71
     *
72
     * @param  Member $member
73
     * @return ValidationResult|Member
74
     */
75
    public function checkNoYubiLogins(Member $member)
76
    {
77
        $maxNoYubi = static::config()->get('MaxNoYubiLogin');
78
        if ($maxNoYubi > 0 && $maxNoYubi <= $member->NoYubikeyCount) {
79
            $validationResult = ValidationResult::create();
80
            $validationResult->addError(
81
                _t(
82
                    self::class . '.ERRORMAXYUBIKEY',
83
                    'Maximum login without yubikey exceeded'
84
                )
85
            );
86
87
            $member->registerFailedLogin();
88
89
            return $validationResult;
90
        }
91
92
        return $member;
93
    }
94
95
    /**
96
     * Check if the member is allowed login after so many days of not using a yubikey
97
     *
98
     * @param  Member $member
99
     * @return ValidationResult|Member
100
     */
101
    public function checkNoYubiDays(Member $member)
102
    {
103
        $date1 = new DateTime($member->Created);
104
        $date2 = new DateTime(date('Y-m-d'));
105
106
        $diff = $date2->diff($date1)->format("%a");
107
        $maxNoYubiDays = static::config()->get('MaxNoYubiLoginDays');
108
109
        if ($maxNoYubiDays > 0 && $diff >= $maxNoYubiDays) {
110
            $validationResult = ValidationResult::create();
111
            $validationResult->addError(
112
                _t(
113
                    self::class . '.ERRORMAXYUBIKEYDAYS',
114
                    'Maximum days without yubikey exceeded'
115
                )
116
            );
117
            $member->registerFailedLogin();
118
119
            return $validationResult;
120
        }
121
122
        return $member;
123
    }
124
125
    /**
126
     * @param $data
127
     * @param $member
128
     * @param ValidationResult $result
129
     * @return ValidationResult|Member
130
     */
131
    public function checkYubikey($data, $member, ValidationResult $result)
132
    {
133
        return $this->authenticateYubikey($data, $member, $result);
134
    }
135
136
    /**
137
     * Validate a member plus it's yubikey login. It compares the fingerprintt and after that,
138
     * tries to validate the Yubikey string
139
     *
140
     * @todo improve this, it's a bit overly complicated
141
     * @todo use the ValidationResult as e reference instead of returning
142
     *
143
     * @param  array $data
144
     * @param  Member $member
145
     * @param ValidationResult $validationResult
146
     * @return ValidationResult|Member
147
     */
148
    private function authenticateYubikey($data, $member, ValidationResult $validationResult)
149
    {
150
        $yubiCode = QwertyConvertor::convertString($data['yubiauth']);
151
        $yubiFingerprint = substr($yubiCode, 0, -32);
152
153
        if ($member->Yubikey) {
154
            $this->validateToken($member, $yubiFingerprint, $validationResult);
155
            if (!$validationResult->isValid()) {
156
                $member->registerFailedLogin();
157
158
                return $validationResult;
159
            }
160
        }
161
        try {
162
            /** @var Response $result */
163
            $result = $this->service->check($yubiCode);
164
165
            // Only check if the call itself doesn't throw an error
166
            if ($result->success() === true) {
167
                $this->updateMember($member, $yubiFingerprint);
168
169
                return $member;
170
            }
171
        } catch (Exception $e) {
172
            $validationResult->addError($e->getMessage());
173
174
            $member->registerFailedLogin();
175
176
            return $validationResult;
177
        }
178
179
        $validationResult->addError(_t(self::class . '.ERROR', 'Yubikey authentication error'));
180
        $member->registerFailedLogin();
181
182
        return $validationResult;
183
    }
184
185
    /**
186
     * Check if the yubikey is unique and linked to the member trying to logon
187
     *
188
     * @param  Member $member
189
     * @param  string $yubiFingerprint
190
     * @param ValidationResult $validationResult
191
     * @return void
192
     */
193
    public function validateToken(Member $member, $yubiFingerprint, ValidationResult $validationResult)
194
    {
195
        /** @var DataList|Member[] $yubikeyMembers */
196
        $yubikeyMembers = Member::get()->filter(['Yubikey' => $yubiFingerprint]);
197
198
        $this->validateMemberCount($member, $yubikeyMembers, $validationResult);
199
        // Yubikeys have a unique fingerprint, if we find a different member with this yubikey ID, something's wrong
200
        $this->validateMemberID($member, $yubikeyMembers, $validationResult);
201
202
        // If the member has a yubikey ID set, compare it to the fingerprint.
203
        $this->validateFingerprint($member, $yubiFingerprint, $validationResult);
204
    }
205
206
    /**
207
     * @param Member $member
208
     * @param DataList|Member[] $yubikeyMembers
209
     * @param ValidationResult $validationResult
210
     */
211
    protected function validateMemberCount(
212
        Member $member,
213
        DataList $yubikeyMembers,
214
        ValidationResult $validationResult
215
    ) {
216
        if ($yubikeyMembers->count() > 1) {
217
            $validationResult->addError(
218
                _t(
219
                    self::class . '.DUPLICATE',
220
                    'Yubikey is duplicate, contact your administrator as soon as possible!'
221
                )
222
            );
223
            $member->registerFailedLogin();
224
        }
225
    }
226
227
    /**
228
     * @param Member $member
229
     * @param DataList|Member[] $yubikeyMembers
230
     * @param ValidationResult $validationResult
231
     */
232
    protected function validateMemberID(Member $member, DataList $yubikeyMembers, ValidationResult $validationResult)
233
    {
234
        if ((int)$yubikeyMembers->count() === 1 && (int)$yubikeyMembers->first()->ID !== (int)$member->ID) {
235
            $validationResult->addError(_t(self::class . '.NOMATCHID', 'Yubikey does not match found member ID'));
236
            $member->registerFailedLogin();
237
        }
238
    }
239
240
    /**
241
     * @param Member $member
242
     * @param $fingerPrint
243
     * @param ValidationResult $validationResult
244
     */
245
    protected function validateFingerprint(Member $member, $fingerPrint, ValidationResult $validationResult)
246
    {
247
        if ($member->Yubikey && strpos($fingerPrint, $member->Yubikey) !== 0) {
248
            $member->registerFailedLogin();
249
            $validationResult->addError(
250
                _t(
251
                    self::class . '.NOMATCH',
252
                    'Yubikey fingerprint does not match found member'
253
                )
254
            );
255
        }
256
    }
257
258
    /**
259
     * Update the member to forcefully enable YubiAuth
260
     * Also, register the Yubikey to the member.
261
     * Documentation:
262
     * https://developers.yubico.com/yubikey-val/Getting_Started_Writing_Clients.html
263
     *
264
     * @param Member $member
265
     * @param string $yubiString The Identifier String of the Yubikey
266
     * @throws ValidationException
267
     */
268
    private function updateMember($member, $yubiString)
269
    {
270
        $member->registerSuccessfulLogin();
271
        $member->NoYubikeyCount = 0;
272
273
        if (!$member->MFAEnabled) {
274
            $member->MFAEnabled = true;
275
        }
276
        if (!$member->Yubikey) {
277
            $member->Yubikey = $yubiString;
278
        }
279
        $member->write();
280
    }
281
282
    /**
283
     * @return Validate
284
     */
285
    public function getService()
286
    {
287
        return $this->service;
288
    }
289
290
    /**
291
     * @param Validate $service
292
     */
293
    public function setService($service)
294
    {
295
        $this->service = $service;
296
    }
297
}
298