Passed
Pull Request — master (#13)
by Simon
03:33 queued 01:31
created

YubikeyMemberAuthenticator   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 225
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 26
dl 0
loc 225
rs 10
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setProvider() 0 5 1
A supportedServices() 0 5 1
A getProvider() 0 3 1
A get_name() 0 3 1
A __construct() 0 4 2
A authenticateNoYubikey() 0 10 2
C validateYubikey() 0 28 7
A updateMember() 0 12 3
B authenticateYubikey() 0 39 6
A getLoginHandler() 0 3 1
A checkYubikey() 0 12 1
1
<?php
2
3
namespace Firesphere\YubiAuth\Authenticators;
4
5
use Exception;
6
use Firesphere\BootstrapMFA\Authenticators\BootstrapMFAAuthenticator;
7
use Firesphere\YubiAuth\Handlers\YubikeyLoginHandler;
8
use Firesphere\YubiAuth\Helpers\QwertyConvertor;
9
use Firesphere\YubiAuth\Providers\YubikeyAuthProvider;
10
use SilverStripe\Control\HTTPRequest;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Core\Environment;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\ORM\ValidationResult;
15
use SilverStripe\Security\Authenticator;
16
use SilverStripe\Security\Member;
17
use Yubikey\Response;
18
use Yubikey\Validate;
19
20
/**
21
 * Class YubikeyAuthenticator
22
 *
23
 * Enable Yubikey Authentication for SilverStripe CMS and member-protected pages.
24
 */
25
class YubikeyMemberAuthenticator extends BootstrapMFAAuthenticator
26
{
27
28
    /**
29
     * @var Validate
30
     */
31
    protected $yubiService;
32
    /**
33
     * @var YubikeyAuthProvider
34
     */
35
    protected $provider;
36
    /**
37
     * @var string
38
     */
39
    private $authenticatorName = 'yubiauth';
0 ignored issues
show
introduced by
The private property $authenticatorName is not used, and could be removed.
Loading history...
40
41
    /**
42
     * Set the provider to a YubikeyAuthProvider instance
43
     *
44
     * YubikeyMemberAuthenticator constructor.
45
     */
46
    public function __construct()
47
    {
48
        if (!$this->provider) {
49
            $this->provider = Injector::inst()->get(YubikeyAuthProvider::class);
50
        }
51
    }
52
53
    /**
54
     * Name of this authenticator
55
     *
56
     * @return string
57
     */
58
    public static function get_name()
59
    {
60
        return _t('YubikeyAuthenticator.TITLE', 'Yubikey 2 factor login');
61
    }
62
63
    /**
64
     * @return YubikeyAuthProvider
65
     */
66
    public function getProvider()
67
    {
68
        return $this->provider;
69
    }
70
71
    /**
72
     * @param YubikeyAuthProvider $provider
73
     * @return $this
74
     */
75
    public function setProvider($provider)
76
    {
77
        $this->provider = $provider;
78
79
        return $this;
80
    }
81
82
    public function supportedServices()
83
    {
84
        // Bitwise-OR of all the supported services in this Authenticator, to make a bitmask
85
        return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD
86
            | Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD;
87
    }
88
89
    /**
90
     * @inheritdoc
91
     *
92
     * @param array $data
93
     * @param HTTPRequest $request
94
     * @param ValidationResult $validationResult
95
     *
96
     * @return ValidationResult|Member
97
     * @throws \SilverStripe\ORM\ValidationException
98
     */
99
    public function validateYubikey($data, $request, &$validationResult = null)
100
    {
101
        if (!$validationResult instanceof ValidationResult) {
102
            $validationResult = ValidationResult::create();
103
        }
104
105
        $memberID = $request->getSession()->get('YubikeyLoginHandler.MemberID');
106
        // First, let's see if we know the member
107
        /** @var Member|null $member */
108
        $member = Member::get()->filter(['ID' => $memberID])->first();
109
110
        // Continue if we have a valid member
111
        if ($member && $member instanceof Member) {
112
113
            // We do not have to check the YubiAuth for now.
114
            if (!$member->YubiAuthEnabled && empty($data['yubiauth'])) {
115
                return $this->authenticateNoYubikey($member);
116
            }
117
118
            // If we know the member, and it's YubiAuth enabled, continue.
119
            if (!empty($data['yubiauth'])) {
120
                return $this->checkYubikey($data, $member);
121
            }
122
            $member->registerFailedLogin();
123
            $validationResult->addError('Yubikey Authentication error');
124
        }
125
126
        return null;
127
    }
128
129
    /**
130
     * Handle login if the user did not enter a Yubikey string.
131
     * Will break out and return NULL if the member should use their Yubikey
132
     *
133
     * @param  Member $member
134
     * @return ValidationResult|Member
135
     * @throws \SilverStripe\ORM\ValidationException
136
     */
137
    private function authenticateNoYubikey($member)
138
    {
139
        ++$member->NoYubikeyCount;
140
        $member->write();
141
        $yubiAuthNoYubi = $this->provider->checkNoYubiAttempts($member);
142
        if ($yubiAuthNoYubi instanceof ValidationResult) {
143
            return $yubiAuthNoYubi;
144
        }
145
146
        return $member;
147
    }
148
149
    /**
150
     * Validate a member plus it's yubikey login. It compares the fingerprintt and after that,
151
     * tries to validate the Yubikey string
152
     *
153
     * @param  array $data
154
     * @param  Member $member
155
     * @return ValidationResult|Member
156
     * @throws \SilverStripe\ORM\ValidationException
157
     */
158
    private function authenticateYubikey($data, $member)
159
    {
160
        if ($url = Config::inst()->get(self::class, 'AuthURL')) {
161
            $this->yubiService->setHost($url);
162
        }
163
        $yubiCode = QwertyConvertor::convertString($data['yubiauth']);
164
        $yubiFingerprint = substr($yubiCode, 0, -32);
165
        $validationResult = ValidationResult::create();
166
167
        if ($member->Yubikey) {
168
            $validationResult = $this->provider->validateYubikey($member, $yubiFingerprint);
169
            if (!$validationResult->isValid()) {
170
                $member->registerFailedLogin();
171
172
                return $validationResult;
173
            }
174
        }
175
        try {
176
            /** @var Response $result */
177
            $result = $this->yubiService->check($yubiCode);
178
            $this->updateMember($member, $yubiFingerprint);
179
        } catch (Exception $e) {
180
            $validationResult->addError($e->getMessage());
181
182
            $member->registerFailedLogin();
183
184
            return $validationResult;
185
        }
186
        if ($result->success() === true) {
187
            $this->updateMember($member, $yubiFingerprint);
188
189
            return $member;
190
        }
191
192
        $validationResult = ValidationResult::create();
193
        $validationResult->addError(_t('YubikeyAuthenticator.ERROR', 'Yubikey authentication error'));
194
        $member->registerFailedLogin();
195
196
        return $validationResult;
197
    }
198
199
    /**
200
     * Update the member to forcefully enable YubiAuth
201
     * Also, register the Yubikey to the member.
202
     * Documentation:
203
     * https://developers.yubico.com/yubikey-val/Getting_Started_Writing_Clients.html
204
     *
205
     * @param Member $member
206
     * @param string $yubiString The Identifier String of the Yubikey
207
     * @throws \SilverStripe\ORM\ValidationException
208
     */
209
    private function updateMember($member, $yubiString)
210
    {
211
        $member->registerSuccessfulLogin();
212
        $member->NoYubikeyCount = 0;
213
214
        if (!$member->YubiAuthEnabled) {
215
            $member->YubiAuthEnabled = true;
216
        }
217
        if (!$member->Yubikey) {
218
            $member->Yubikey = $yubiString;
219
        }
220
        $member->write();
221
    }
222
223
224
    /**
225
     * @param string $link
226
     * @return \SilverStripe\Security\MemberAuthenticator\LoginHandler|static
227
     */
228
    public function getLoginHandler($link)
229
    {
230
        return YubikeyLoginHandler::create($link, $this);
231
    }
232
233
    /**
234
     * @param $data
235
     * @param $member
236
     * @return ValidationResult|Member
237
     */
238
    protected function checkYubikey($data, $member)
239
    {
240
        /** @var Validate $service */
241
        $this->yubiService = Injector::inst()->createWithArgs(
242
            Validate::class,
243
            [
244
                Environment::getEnv('YUBIAUTH_APIKEY'),
245
                Environment::getEnv('YUBIAUTH_CLIENTID'),
246
            ]
247
        );
248
249
        return $this->authenticateYubikey($data, $member);
250
    }
251
}
252