Completed
Push — master ( 2fdb91...d84985 )
by Simon
01:50
created

YubikeyAuthenticator::authenticate()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 29
rs 6.7272
cc 7
eloc 18
nc 5
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
                    self::$yubiService->setHost($url);
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
        $yubiCode = QwertyConvertor::convertString($data['Yubikey']);
139
        $yubiFingerprint = substr($yubiCode, 0, -32);
140
        if (!self::validateYubikey($member, $yubiFingerprint)) {
141
            return null;
142
        }
143
        /** @var Response $result */
144
        $result = self::$yubiService->check($yubiCode);
145
146
        if ($result->success() === true) {
147
            self::updateMember($member, $yubiFingerprint);
148
149
            return $member;
150
        } else {
151
            $validationMessage = ValidationResult::create(false,
152
                _t('YubikeyAuthenticator.ERROR', 'Yubikey authentication error'));
153
            self::updateForm($validationMessage);
154
            $member->registerFailedLogin();
155
156
            return null;
157
        }
158
    }
159
160
    /**
161
     * Check if the yubikey is unique and linked to the member trying to logon
162
     *
163
     * @param Member $member
164
     * @param string $yubiFingerprint
165
     * @return boolean
166
     */
167
    private static function validateYubikey($member, $yubiFingerprint)
168
    {
169
        $yubikeyMember = Member::get()->filter(array('Yubikey' => $yubiFingerprint))->first();
170
        // Yubikeys have a unique fingerprint, if we find a different member with this yubikey ID, something's wrong
171 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...
172
            $validationMessage = ValidationResult::create(false,
173
                _t('YubikeyAuthenticator.DUPLICATE', 'Yubikey is duplicate'));
174
            self::updateForm($validationMessage);
175
            $member->registerFailedLogin();
176
177
            return false;
178
        }
179
        // If the member has a yubikey ID set, compare it to the fingerprint.
180
        if ($member->Yubikey && strpos($yubiFingerprint, $member->Yubikey) !== 0) {
181
            self::updateForm();
182
            $member->registerFailedLogin();
183
184
            return false; // Yubikey id doesn't match the member.
185
        }
186
187
        return true;
188
    }
189
190
191
    /**
192
     * Handle login if the user did not enter a Yubikey string.
193
     * Will break out and return NULL if the member should use their Yubikey
194
     *
195
     * @param Member $member
196
     * @return null|Member
197
     */
198
    private static function authenticate_noyubikey($member)
199
    {
200
        ++$member->NoYubikeyCount;
201
        $member->write();
202
        if (!self::checkNoYubiLogins($member) || !self::checkNoYubiDays($member)) {
203
            return null;
204
        }
205
206
        return $member;
207
    }
208
209
    /**
210
     * Check if a member is allowed to login without a yubikey
211
     *
212
     * @param Member $member
213
     * @return bool|Member
214
     */
215
    private static function checkNoYubiLogins($member)
216
    {
217
        $maxNoYubi = self::config()->get('MaxNoYubiLogin');
218 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...
219
            $validationError = ValidationResult::create(false,
220
                _t('YubikeyAuthenticator.ERRORMAXYUBIKEY', 'Maximum login without yubikey exceeded'));
221
            self::updateForm($validationError);
222
            $member->registerFailedLogin();
223
224
            return false;
225
        }
226
227
        return $member;
228
    }
229
230
    /**
231
     * Check if the member is allowed login after so many days of not using a yubikey
232
     *
233
     * @param Member $member
234
     * @return bool|Member
235
     */
236
    private static function checkNoYubiDays($member)
237
    {
238
239
        $date1 = new DateTime($member->Created);
240
        $date2 = new DateTime(date('Y-m-d'));
241
242
        $diff = $date2->diff($date1)->format("%a");
243
        $maxNoYubiDays = self::config()->get('MaxNoYubiLoginDays');
244
245 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...
246
            $validationError = ValidationResult::create(false,
247
                _t('YubikeyAuthenticator.ERRORMAXYUBIKEYDAYS', 'Maximum days without yubikey exceeded'));
248
            self::updateForm($validationError);
249
            $member->registerFailedLogin();
250
251
            return false;
252
        }
253
254
        return $member;
255
    }
256
}