Completed
Branch develop (8f3bda)
by Simon
01:37
created

YubikeyAuthenticator::validateYubikey()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 22
Code Lines 13

Duplication

Lines 8
Ratio 36.36 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 8
loc 22
rs 8.6737
cc 5
eloc 13
nc 3
nop 2
1
<?php
2
namespace Firesphere\YubiAuth;
3
4
use Config;
5
use Controller;
6
use DateTime;
7
use Form;
8
use Injector;
9
use Member;
10
use MemberAuthenticator;
11
use ValidationResult;
12
use Yubikey\Response;
13
use Yubikey\Validate;
14
15
/**
16
 * Class YubikeyAuthenticator
17
 *
18
 * Enable Yubikey Authentication for SilverStripe CMS and member-protected pages.
19
 */
20
class YubikeyAuthenticator extends MemberAuthenticator
21
{
22
    /**
23
     * @var null|Form
24
     */
25
    protected static $form;
26
27
    /**
28
     * @var Validate
29
     */
30
    protected static $yubiService;
31
32
    /**
33
     * @inheritdoc
34
     *
35
     * @param array $data
36
     * @param Form|null $form
37
     *
38
     * @return null|Member
39
     */
40
    public static function authenticate($data, Form $form = null)
41
    {
42
        self::$form = $form;
43
        $currentLoginRecording = Config::inst()->get('Security', 'login_recording');
44
        Config::inst()->update('Security', 'login_recording', false); // Disable login_recording for this auth.
45
        // First, let's see if we know the member
46
        $member = parent::authenticate($data, $form);
47
        Config::inst()->update('Security', 'login_recording', $currentLoginRecording); // Reset login_recording
48
        // Continue if we have a valid member
49
        if ($member && $member instanceof Member) {
50
            // If we know the member, and it's YubiAuth enabled, continue.
51
            if ($member->YubiAuthEnabled || $data['Yubikey'] !== '') {
52
                /** @var Validate $service */
53
                self::$yubiService = Injector::inst()->createWithArgs('Yubikey\Validate',
54
                    array(YUBIAUTH_APIKEY, YUBIAUTH_CLIENTID));
55
                if ($url = self::config()->get('AuthURL')) {
56
                    $service->setHost($url);
0 ignored issues
show
Bug introduced by
The variable $service does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
57
                }
58
59
                return self::authenticate_yubikey($data, $member);
60
            } elseif (!$member->YubiAuthEnabled) { // We do not have to check the YubiAuth for now.
61
                return self::authenticate_noyubikey($member);
62
            }
63
            $member->registerFailedLogin();
64
        }
65
        self::updateForm();
66
67
        return null;
68
    }
69
70
    /**
71
     * @param Controller $controller
72
     *
73
     * @return Form
74
     */
75
    public static function get_login_form(Controller $controller)
76
    {
77
        return YubikeyLoginForm::create($controller, 'LoginForm');
78
    }
79
80
    /**
81
     * Name of this authenticator
82
     *
83
     * @return string
84
     */
85
    public static function get_name()
86
    {
87
        return _t('YubikeyAuthenticator.TITLE', 'Yubikey login');
88
    }
89
90
    /**
91
     * Update the member to forcefully enable YubiAuth
92
     * Also, register the Yubikey to the member.
93
     * Documentation:
94
     * https://developers.yubico.com/yubikey-val/Getting_Started_Writing_Clients.html
95
     *
96
     * @param Member $member
97
     * @param string $yubiString The Identifier String of the Yubikey
98
     */
99
    private static function updateMember($member, $yubiString)
100
    {
101
        $member->registerSuccessfulLogin();
102
        $member->NoYubikeyCount = 0;
103
104
        if (!$member->YubiAuthEnabled) {
105
            $member->YubiAuthEnabled = true;
106
        }
107
        if (!$member->Yubikey) {
108
            $member->Yubikey = $yubiString;
109
        }
110
        $member->write();
111
    }
112
113
    /**
114
     * @param null|ValidationResult $validation
115
     */
116
    private static function updateForm($validation = null)
117
    {
118
        $form = self::$form;
119
        if ($form) {
120
            if ($validation === null) {
121
                // Default validation error.
122
                $validation = ValidationResult::create(false,
123
                    _t('YubikeyAuthenticator.ERRORYUBIKEY', 'Yubikey authentication error'));
124
            }
125
            $form->sessionMessage($validation->message(), 'bad');
126
        }
127
128
    }
129
130
    /**
131
     * Validate a member plus it's yubikey login. It compares the fingerprintt and after that, tries to validate the Yubikey string
132
     * @param array $data
133
     * @param Member $member
134
     * @return null|Member
135
     */
136
    private static function authenticate_yubikey($data, $member)
137
    {
138
        $data['Yubikey'] = strtolower($data['Yubikey']);
139
        $yubiCode = QwertyConvertor::convertString($data['Yubikey']);
140
        $yubiFingerprint = substr($yubiCode, 0, -32);
141
        if (!self::validateYubikey($member, $yubiFingerprint)) {
142
            return null;
143
        }
144
        /** @var Response $result */
145
        $result = self::$yubiService->check($yubiCode);
146
147
        if ($result->success() === true) {
148
            self::updateMember($member, $yubiFingerprint);
149
150
            return $member;
151
        } else {
152
            $validationMessage = ValidationResult::create(false,
153
                _t('YubikeyAuthenticator.ERROR', 'Yubikey authentication error'));
154
            self::updateForm($validationMessage);
155
            $member->registerFailedLogin();
156
157
            return null;
158
        }
159
    }
160
161
    /**
162
     * Check if the yubikey is unique and linked to the member trying to logon
163
     *
164
     * @param Member $member
165
     * @param string $yubiFingerprint
166
     * @return boolean
167
     */
168
    private static function validateYubikey($member, $yubiFingerprint)
169
    {
170
        $yubikeyMember = Member::get()->filter(array('Yubikey' => $yubiFingerprint))->first();
171
        // Yubikeys have a unique fingerprint, if we find a different member with this yubikey ID, something's wrong
172 View Code Duplication
        if ($yubikeyMember && $yubikeyMember->ID !== $member->ID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
            $validationMessage = ValidationResult::create(false,
174
                _t('YubikeyAuthenticator.DUPLICATE', 'Yubikey is duplicate'));
175
            self::updateForm($validationMessage);
176
            $member->registerFailedLogin();
177
178
            return false;
179
        }
180
        // If the member has a yubikey ID set, compare it to the fingerprint.
181
        if ($member->Yubikey && strpos($yubiFingerprint, $member->Yubikey) !== 0) {
182
            self::updateForm();
183
            $member->registerFailedLogin();
184
185
            return false; // Yubikey id doesn't match the member.
186
        }
187
188
        return true;
189
    }
190
191
192
    /**
193
     * Handle login if the user did not enter a Yubikey string.
194
     * Will break out and return NULL if the member should use their Yubikey
195
     *
196
     * @param Member $member
197
     * @return null|Member
198
     */
199
    private static function authenticate_noyubikey($member)
200
    {
201
        ++$member->NoYubikeyCount;
202
        $member->write();
203
        if (!self::checkNoYubiLogins($member) || !self::checkNoYubiDays($member)) {
204
            return null;
205
        }
206
207
        return $member;
208
    }
209
210
    /**
211
     * Check if a member is allowed to login without a yubikey
212
     *
213
     * @param Member $member
214
     * @return bool|Member
215
     */
216
    private static function checkNoYubiLogins($member)
217
    {
218
        $maxNoYubi = self::config()->get('MaxNoYubiLogin');
219 View Code Duplication
        if ($maxNoYubi > 0 && $maxNoYubi <= $member->NoYubikeyCount) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
            $validationError = ValidationResult::create(false,
221
                _t('YubikeyAuthenticator.ERRORMAXYUBIKEY', 'Maximum login without yubikey exceeded'));
222
            self::updateForm($validationError);
223
            $member->registerFailedLogin();
224
225
            return false;
226
        }
227
228
        return $member;
229
    }
230
231
    /**
232
     * Check if the member is allowed login after so many days of not using a yubikey
233
     *
234
     * @param Member $member
235
     * @return bool|Member
236
     */
237
    private static function checkNoYubiDays($member)
238
    {
239
240
        $date1 = new DateTime($member->Created);
241
        $date2 = new DateTime(date('Y-m-d'));
242
243
        $diff = $date2->diff($date1)->format("%a");
244
        $maxNoYubiDays = self::config()->get('MaxNoYubiLoginDays');
245
246 View Code Duplication
        if ($maxNoYubiDays > 0 && $diff >= $maxNoYubiDays) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247
            $validationError = ValidationResult::create(false,
248
                _t('YubikeyAuthenticator.ERRORMAXYUBIKEYDAYS', 'Maximum days without yubikey exceeded'));
249
            self::updateForm($validationError);
250
            $member->registerFailedLogin();
251
252
            return false;
253
        }
254
255
        return $member;
256
    }
257
}