Passed
Pull Request — master (#13)
by Simon
01:47
created

YubikeyMemberAuthenticator::validateYubikey()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 33
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 33
rs 8.439
cc 6
eloc 14
nc 4
nop 3

4 Methods

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