Passed
Push — master ( 56002b...d843de )
by Simon
01:21
created

YubikeyMemberAuthenticator::validateYubikey()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 32
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 32
rs 8.439
cc 6
eloc 13
nc 4
nop 3
1
<?php
2
3
namespace Firesphere\YubiAuth;
4
5
use Exception;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\Session;
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\Core\Environment;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Dev\Debug;
12
use SilverStripe\ORM\ValidationResult;
13
use SilverStripe\Security\Authenticator;
14
use SilverStripe\Security\Member;
15
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
16
use Yubikey\Response;
17
use Yubikey\Validate;
18
19
/**
20
 * Class YubikeyAuthenticator
21
 *
22
 * Enable Yubikey Authentication for SilverStripe CMS and member-protected pages.
23
 */
24
class YubikeyMemberAuthenticator extends MemberAuthenticator
25
{
26
27
    public function supportedServices()
28
    {
29
        // Bitwise-OR of all the supported services in this Authenticator, to make a bitmask
30
        return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD
31
            | Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD;
32
    }
33
    /**
34
     * @var Validate
35
     */
36
    protected $yubiService;
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
     * @inheritdoc
42
     *
43
     * @param array   $data
44
     * @param HTTPRequest $request
45
     * @param $message
46
     *
47
     * @return null|Member
48
     */
49
    public function validateYubikey($data, $request, &$message)
50
    {
51
        $memberID = $request->getSession()->get('YubikeyLoginHandler.MemberID');
52
        // First, let's see if we know the member
53
        /** @var Member $member */
54
        $member = Member::get()->filter(['ID' => $memberID])->first();
55
56
        // Continue if we have a valid member
57
        if ($member && $member instanceof Member) {
58
59
            // We do not have to check the YubiAuth for now.
60
            if (!$member->YubiAuthEnabled && empty($data['yubiauth'])) {
61
                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...
62
            }
63
64
            // If we know the member, and it's YubiAuth enabled, continue.
65
            if (!empty($data['yubiauth'])) {
66
                /** @var Validate $service */
67
                $this->yubiService = Injector::inst()->createWithArgs(Validate::class,
68
                    [
69
                        Environment::getEnv('YUBIAUTH_APIKEY'),
70
                        Environment::getEnv('YUBIAUTH_CLIENTID'),
71
                    ]
72
                );
73
74
                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...
75
            }
76
            $member->registerFailedLogin();
77
            $message = 'Yubikey Authentication error';
78
        }
79
80
        return null;
81
    }
82
83
    /**
84
     * Name of this authenticator
85
     *
86
     * @return string
87
     */
88
    public static function get_name()
89
    {
90
        return _t('YubikeyAuthenticator.TITLE', 'Yubikey 2 factor login');
91
    }
92
93
    /**
94
     * Validate a member plus it's yubikey login. It compares the fingerprintt and after that,
95
     * tries to validate the Yubikey string
96
     *
97
     * @param  array  $data
98
     * @param  Member $member
99
     * @return ValidationResult|Member
100
     */
101
    private function authenticateYubikey($data, $member)
102
    {
103
        if ($url = Config::inst()->get(self::class, 'AuthURL')) {
104
            $this->yubiService->setHost($url);
105
        }
106
        $yubiCode = QwertyConvertor::convertString($data['yubiauth']);
107
        $yubiFingerprint = substr($yubiCode, 0, -32);
108
        if ($member->Yubikey) {
109
            $validateYubiMember = YubiAuthProvider::validateYubikey($member, $yubiFingerprint);
110
            if ($validateYubiMember instanceof ValidationResult) {
111
                $member->registerFailedLogin();
112
113
                return $validateYubiMember;
114
            }
115
        }
116
        try {
117
            /** @var Response $result */
118
            $result = $this->yubiService->check($yubiCode);
119
            $this->updateMember($member, $yubiFingerprint);
120
        } catch (Exception $e) {
121
            $validationResult = ValidationResult::create();
122
            $validationResult->addError($e->getMessage());
123
124
            $member->registerFailedLogin();
125
126
            return $validationResult;
127
        }
128
        if ($result->success() === true) {
129
            $this->updateMember($member, $yubiFingerprint);
130
131
            return $member;
132
        }
133
134
        $validationResult = ValidationResult::create();
135
        $validationResult->addError(_t('YubikeyAuthenticator.ERROR', 'Yubikey authentication error'));
136
        $member->registerFailedLogin();
137
138
        return $validationResult;
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 = YubiAuthProvider::checkNoYubiAttempts($member);
153
        if ($yubiAuthNoYubi instanceof ValidationResult) {
154
155
            return $yubiAuthNoYubi;
156
        }
157
158
        return $member;
159
    }
160
161
    /**
162
     * Update the member to forcefully enable YubiAuth
163
     * Also, register the Yubikey to the member.
164
     * Documentation:
165
     * https://developers.yubico.com/yubikey-val/Getting_Started_Writing_Clients.html
166
     *
167
     * @param Member $member
168
     * @param string $yubiString The Identifier String of the Yubikey
169
     */
170
    private function updateMember($member, $yubiString)
171
    {
172
        $member->registerSuccessfulLogin();
173
        $member->NoYubikeyCount = 0;
174
175
        if (!$member->YubiAuthEnabled) {
176
            $member->YubiAuthEnabled = true;
177
        }
178
        if (!$member->Yubikey) {
179
            $member->Yubikey = $yubiString;
180
        }
181
        $member->write();
182
    }
183
184
185
    public function getLoginHandler($link)
186
    {
187
        return YubikeyLoginHandler::create($link, $this);
0 ignored issues
show
Bug introduced by
$this of type Firesphere\YubiAuth\YubikeyMemberAuthenticator is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
        return YubikeyLoginHandler::create($link, /** @scrutinizer ignore-type */ $this);
Loading history...
188
    }
189
}
190