Passed
Push — master ( 0aa220...4a8f3f )
by Simon
01:49
created

YubikeyAuthProvider::setService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 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(self::class, 'AuthURL')) {
50
            if (!is_array($url)) {
51
                $url = [$url];
52
            }
53
            $this->service->setHosts($url);
54
        }
55
    }
56
57
    /**
58
     * @param Member $member
59
     * @return ValidationResult|Member
60
     */
61
    public function checkNoYubiAttempts(Member $member)
62
    {
63
        $noYubiLogins = $this->checkNoYubiLogins($member);
64
        if ($noYubiLogins instanceof Member) {
65
            return $this->checkNoYubiDays($member);
66
        }
67
68
        return $noYubiLogins;
69
    }
70
71
    /**
72
     * Check if a member is allowed to login without a yubikey
73
     *
74
     * @param  Member $member
75
     * @return ValidationResult|Member
76
     */
77
    public function checkNoYubiLogins(Member $member)
78
    {
79
        $maxNoYubi = static::config()->get('MaxNoYubiLogin');
80
        if ($maxNoYubi > 0 && $maxNoYubi <= $member->NoYubikeyCount) {
81
            $validationResult = ValidationResult::create();
82
            $validationResult->addError(
83
                _t(
84
                    self::class . '.ERRORMAXYUBIKEY',
85
                    'Maximum login without yubikey exceeded'
86
                )
87
            );
88
89
            $member->registerFailedLogin();
90
91
            return $validationResult;
92
        }
93
94
        return $member;
95
    }
96
97
    /**
98
     * Check if the member is allowed login after so many days of not using a yubikey
99
     *
100
     * @param  Member $member
101
     * @return ValidationResult|Member
102
     */
103
    public function checkNoYubiDays(Member $member)
104
    {
105
        $date1 = new DateTime($member->Created);
106
        $date2 = new DateTime(date('Y-m-d'));
107
108
        $diff = $date2->diff($date1)->format("%a");
109
        $maxNoYubiDays = static::config()->get('MaxNoYubiLoginDays');
110
111
        if ($maxNoYubiDays > 0 && $diff >= $maxNoYubiDays) {
112
            $validationResult = ValidationResult::create();
113
            $validationResult->addError(
114
                _t(
115
                    self::class . '.ERRORMAXYUBIKEYDAYS',
116
                    'Maximum days without yubikey exceeded'
117
                )
118
            );
119
            $member->registerFailedLogin();
120
121
            return $validationResult;
122
        }
123
124
        return $member;
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
        return $this->authenticateYubikey($data, $member, $result);
137
    }
138
139
    /**
140
     * Validate a member plus it's yubikey login. It compares the fingerprintt and after that,
141
     * tries to validate the Yubikey string
142
     *
143
     * @todo improve this, it's a bit overly complicated
144
     * @todo use the ValidationResult as e reference instead of returning
145
     *
146
     * @param  array $data
147
     * @param  Member $member
148
     * @return ValidationResult|Member
149
     * @throws ValidationException
150
     */
151
    private function authenticateYubikey($data, $member, ValidationResult &$validationResult)
152
    {
153
        $yubiCode = QwertyConvertor::convertString($data['yubiauth']);
154
        $yubiFingerprint = substr($yubiCode, 0, -32);
155
        $validationResult = ValidationResult::create();
156
157
        if ($member->Yubikey) {
158
            $this->validateToken($member, $yubiFingerprint, $validationResult);
159
            if (!$validationResult->isValid()) {
160
                $member->registerFailedLogin();
161
162
                return $validationResult;
163
            }
164
        }
165
        try {
166
            /** @var Response $result */
167
            $result = $this->service->check($yubiCode);
168
169
            // Only check if the call itself doesn't throw an error
170
            if ($result->success() === true) {
171
                $this->updateMember($member, $yubiFingerprint);
172
173
                return $member;
174
            }
175
        } catch (Exception $e) {
176
            $validationResult->addError($e->getMessage());
177
178
            $member->registerFailedLogin();
179
180
            return $validationResult;
181
        }
182
183
        $validationResult->addError(_t(self::class . '.ERROR', 'Yubikey authentication error'));
184
        $member->registerFailedLogin();
185
186
        return $validationResult;
187
    }
188
189
    /**
190
     * Check if the yubikey is unique and linked to the member trying to logon
191
     *
192
     * @param  Member $member
193
     * @param  string $yubiFingerprint
194
     * @param ValidationResult $validationResult
195
     * @return void
196
     */
197
    public function validateToken(Member $member, $yubiFingerprint, ValidationResult &$validationResult)
198
    {
199
        /** @var DataList|Member[] $yubikeyMembers */
200
        $yubikeyMembers = Member::get()->filter(['Yubikey' => $yubiFingerprint]);
201
202
        /** @var ValidationResult $validationResult */
203
        $validationResult = ValidationResult::create();
204
205
        $this->validateMemberCount($member, $yubikeyMembers, $validationResult);
206
        // Yubikeys have a unique fingerprint, if we find a different member with this yubikey ID, something's wrong
207
        $this->validateMemberID($member, $yubikeyMembers, $validationResult);
208
209
        // If the member has a yubikey ID set, compare it to the fingerprint.
210
        $this->validateFingerprint($member, $yubiFingerprint, $validationResult);
211
    }
212
213
    /**
214
     * @param Member $member
215
     * @param DataList|Member[] $yubikeyMembers
216
     * @param ValidationResult $validationResult
217
     */
218
    protected function validateMemberCount(
219
        Member $member,
220
        DataList $yubikeyMembers,
221
        ValidationResult $validationResult
222
    ) {
223
        if ($yubikeyMembers->count() > 1) {
224
            $validationResult->addError(
225
                _t(
226
                    self::class . '.DUPLICATE',
227
                    'Yubikey is duplicate, contact your administrator as soon as possible!'
228
                )
229
            );
230
            $member->registerFailedLogin();
231
        }
232
    }
233
234
    /**
235
     * @param Member $member
236
     * @param DataList|Member[] $yubikeyMembers
237
     * @param ValidationResult $validationResult
238
     */
239
    protected function validateMemberID(Member $member, DataList $yubikeyMembers, ValidationResult $validationResult)
240
    {
241
        if ((int)$yubikeyMembers->count() === 1 && (int)$yubikeyMembers->first()->ID !== (int)$member->ID) {
242
            $validationResult->addError(_t(self::class . '.NOMATCHID', 'Yubikey does not match found member ID'));
243
            $member->registerFailedLogin();
244
        }
245
    }
246
247
    /**
248
     * @param Member $member
249
     * @param $fingerPrint
250
     * @param ValidationResult $validationResult
251
     */
252
    protected function validateFingerprint(Member $member, $fingerPrint, ValidationResult $validationResult)
253
    {
254
        if ($member->Yubikey && strpos($fingerPrint, $member->Yubikey) !== 0) {
255
            $member->registerFailedLogin();
256
            $validationResult->addError(
257
                _t(
258
                    self::class . '.NOMATCH',
259
                    'Yubikey fingerprint does not match found member'
260
                )
261
            );
262
        }
263
    }
264
265
    /**
266
     * Update the member to forcefully enable YubiAuth
267
     * Also, register the Yubikey to the member.
268
     * Documentation:
269
     * https://developers.yubico.com/yubikey-val/Getting_Started_Writing_Clients.html
270
     *
271
     * @param Member $member
272
     * @param string $yubiString The Identifier String of the Yubikey
273
     * @throws ValidationException
274
     */
275
    private function updateMember($member, $yubiString)
276
    {
277
        $member->registerSuccessfulLogin();
278
        $member->NoYubikeyCount = 0;
279
280
        if (!$member->MFAEnabled) {
281
            $member->MFAEnabled = true;
282
        }
283
        if (!$member->Yubikey) {
284
            $member->Yubikey = $yubiString;
285
        }
286
        $member->write();
287
    }
288
289
    /**
290
     * @return Validate
291
     */
292
    public function getService()
293
    {
294
        return $this->service;
295
    }
296
297
    /**
298
     * @param Validate $service
299
     */
300
    public function setService($service)
301
    {
302
        $this->service = $service;
303
    }
304
}
305