Passed
Pull Request — master (#13)
by Simon
02:03
created

YubikeyMemberAuthenticator::validateYubikey()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 28
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 12
nc 8
nop 3
1
<?php
2
3
namespace Firesphere\YubiAuth\Authenticators;
4
5
use Exception;
6
use Firesphere\BootstrapMFA\Authenticators\BootstrapMFAAuthenticator;
7
use Firesphere\BootstrapMFA\Handlers\BootstrapMFALoginHandler;
0 ignored issues
show
Bug introduced by
The type Firesphere\BootstrapMFA\...ootstrapMFALoginHandler was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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