Passed
Push — master ( 05a7c1...0aa220 )
by Simon
01:55
created

YubikeyAuthProvider::authenticateYubikey()   B

Complexity

Conditions 6
Paths 22

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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