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

YubikeyMemberAuthenticator::validateYubikey()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 37
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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