Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace factor_webauthn;
18
 
19
defined('MOODLE_INTERNAL') || die();
20
 
21
require_once($CFG->libdir . '/webauthn/src/WebAuthn.php');
22
 
23
use lbuchs\WebAuthn\Binary\ByteBuffer;
24
use lbuchs\WebAuthn\WebAuthn;
25
use lbuchs\WebAuthn\WebAuthnException;
26
use stdClass;
27
use tool_mfa\local\factor\object_factor_base;
28
 
29
/**
30
 * WebAuthn factor class.
31
 *
32
 * @package     factor_webauthn
33
 * @author      Alex Morris <alex.morris@catalyst.net.nz>
34
 * @copyright   Catalyst IT
35
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 */
37
class factor extends object_factor_base {
38
 
39
    /** @var WebAuthn WebAuthn server */
40
    private $webauthn;
41
    /** @var string Relying party ID */
42
    private $rpid;
43
    /** @var string User verification setting */
44
    private $userverification;
45
 
46
    /** @var string Factor icon */
47
    protected $icon = 'fa-hand-pointer';
48
 
49
    /**
50
     * Create webauthn server.
51
     *
52
     * @param string $name
53
     */
54
    public function __construct($name) {
55
        global $CFG, $SITE;
56
        parent::__construct($name);
57
 
58
        $this->rpid = (new \moodle_url($CFG->wwwroot))->get_host();
59
        $this->webauthn = new WebAuthn($SITE->fullname, $this->rpid);
60
 
61
        $this->userverification = get_config('factor_webauthn', 'userverification');
62
    }
63
 
64
    /**
65
     * WebAuthn Factor implementation.
66
     *
67
     * @param stdClass $user the user to check against.
68
     * @return array
69
     */
70
    public function get_all_user_factors(stdClass $user): array {
71
        global $DB;
72
        return $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
73
    }
74
 
75
    /**
76
     * WebAuthn Factor implementation.
77
     *
78
     * {@inheritDoc}
79
     */
80
    public function has_input(): bool {
81
        return true;
82
    }
83
 
84
    /**
85
     * WebAuthn Factor implementation.
86
     *
87
     * {@inheritDoc}
88
     */
89
    public function has_revoke(): bool {
90
        return true;
91
    }
92
 
93
    /**
94
     * WebAuthn Factor implementation.
95
     */
96
    public function has_replace(): bool {
97
        return true;
98
    }
99
 
100
    /**
101
     * WebAuthn Factor implementation.
102
     *
103
     * {@inheritDoc}
104
     */
105
    public function has_setup(): bool {
106
        return true;
107
    }
108
 
109
    /**
110
     * WebAuthn Factor implementation.
111
     *
112
     * {@inheritDoc}
113
     */
114
    public function show_setup_buttons(): bool {
115
        return true;
116
    }
117
 
118
    /**
119
     * WebAuthn factor implementation.
120
     *
121
     * @param stdClass $user
122
     * @return array
123
     */
124
    public function possible_states(stdClass $user): array {
125
        return [
126
            \tool_mfa\plugininfo\factor::STATE_PASS,
127
            \tool_mfa\plugininfo\factor::STATE_NEUTRAL,
128
            \tool_mfa\plugininfo\factor::STATE_UNKNOWN,
129
        ];
130
    }
131
 
132
    /**
133
     * WebAuthn state
134
     *
135
     * {@inheritDoc}
136
     */
137
    public function get_state(): string {
138
        global $USER;
139
        $userfactors = $this->get_active_user_factors($USER);
140
 
141
        // If no authenticators are set up then we are neutral not unknown.
142
        if (count($userfactors) == 0) {
143
            return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
144
        }
145
 
146
        return parent::get_state();
147
    }
148
 
149
    /**
150
     * Gets the string for setup button on preferences page.
151
     *
152
     * @return string
153
     */
154
    public function get_setup_string(): string {
155
        return get_string('setupfactorbutton', 'factor_webauthn');
156
    }
157
 
158
    /**
159
     * Gets the string for manage button on preferences page.
160
     *
161
     * @return string
162
     */
163
    public function get_manage_string(): string {
164
        return get_string('managefactorbutton', 'factor_webauthn');
165
    }
166
 
167
    /**
168
     * WebAuthn Factor implementation.
169
     *
170
     * @param \MoodleQuickForm $mform
171
     * @return \MoodleQuickForm $mform
172
     */
173
    public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
174
        global $PAGE, $USER, $SESSION;
175
 
176
        $mform->addElement('hidden', 'response_input', '', ['id' => 'id_response_input']);
177
        $mform->setType('response_input', PARAM_RAW);
178
 
179
        // Required to attach verification errors, so they can be displayed to the user.
180
        $mform->addElement('static', 'verificationcode', '', '');
181
 
182
        $ids = [];
183
 
184
        $authenticators = $this->get_active_user_factors($USER);
185
        foreach ($authenticators as $authenticator) {
186
            $registration = json_decode($authenticator->secret);
187
            $ids[] = base64_decode($registration->credentialId);
188
        }
189
 
190
        $types = explode(',', get_config('factor_webauthn', 'authenticatortypes'));
191
        $getargs =
192
            $this->webauthn->getGetArgs($ids, 20, in_array('usb', $types), in_array('nfc', $types), in_array('ble', $types),
193
                in_array('hybrid', $types), in_array('internal', $types), $this->userverification);
194
 
195
        $PAGE->requires->js_call_amd('factor_webauthn/login', 'init', [json_encode($getargs)]);
196
 
197
        // Challenge is regenerated on form submission, at this point we aren't aware if the form is submitted for being
198
        // loaded for the first time, so we store the existing and new challenge.
199
        if (isset($SESSION->factor_webauthn_challenge_new)) {
200
            $SESSION->factor_webauthn_challenge = $SESSION->factor_webauthn_challenge_new;
201
        }
202
        $SESSION->factor_webauthn_challenge_new = $this->webauthn->getChallenge()->getHex();
203
 
204
        return $mform;
205
    }
206
 
207
    /**
208
     * WebAuthn Factor implementation.
209
     *
210
     * @param array $data
211
     * @return array
212
     */
213
    public function login_form_validation(array $data): array {
214
        global $USER, $SESSION;
215
 
216
        $errors = [];
217
        if (empty($data['response_input'])) {
218
            $errors['verificationcode'] = get_string('error', 'factor_webauthn');
219
            return $errors;
220
        }
221
 
222
        $post = json_decode($data['response_input'], null, 512, JSON_THROW_ON_ERROR);
223
 
224
        $id = base64_decode($post->id);
225
        $clientdata = base64_decode($post->clientDataJSON);
226
        $authenticatordata = base64_decode($post->authenticatorData);
227
        $signature = base64_decode($post->signature);
228
        $credentialpublickey = null;
229
        $challenge = ByteBuffer::fromHex($SESSION->factor_webauthn_challenge);
230
        unset($SESSION->factor_webauthn_challenge);
231
 
232
        $authenticators = $this->get_active_user_factors($USER);
233
        foreach ($authenticators as $authenticator) {
234
            $registration = json_decode($authenticator->secret);
235
            if (base64_decode($registration->credentialId) === $id) {
236
                $credentialpublickey = $registration->credentialPublicKey;
237
                break;
238
            }
239
        }
240
 
241
        if ($credentialpublickey === null) {
242
            $errors['verificationcode'] = get_string('error', 'factor_webauthn');
243
            return $errors;
244
        }
245
 
246
        try {
247
            // Throws exception if authentication fails.
248
            $this->webauthn->processGet($clientdata, $authenticatordata, $signature, $credentialpublickey, $challenge, null,
249
                $this->userverification === 'required');
250
        } catch (WebAuthnException $ex) {
251
            $errors['verificationcode'] = get_string('error', 'factor_webauthn');
252
        }
253
 
254
        return $errors;
255
    }
256
 
257
    /**
258
     * WebAuthn Factor implementation.
259
     *
260
     * @param \MoodleQuickForm $mform
261
     * @return \MoodleQuickForm $mform
262
     */
263
    public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
264
        global $PAGE, $USER, $SESSION, $OUTPUT;
265
 
266
        $headingstring = $mform->elementExists('replaceid') ? 'replacefactor' : 'setupfactor';
267
        $mform->addElement('html', $OUTPUT->heading(get_string($headingstring, 'factor_webauthn'), 2));
268
 
269
        $html = \html_writer::tag('p', get_string('setupfactor:intro', 'factor_webauthn'));
270
        $mform->addElement('html', $html);
271
 
272
        // Security key name.
273
        $mform->addElement('html', \html_writer::tag('p', get_string('setupfactor:instructionssecuritykeyname', 'factor_webauthn'),
274
            ['class' => 'bold']));
275
 
276
        $mform->addElement('text', 'webauthn_name', get_string('authenticatorname', 'factor_webauthn'));
277
        $mform->setType('webauthn_name', PARAM_TEXT);
278
        $mform->addRule('webauthn_name', get_string('required'), 'required', null, 'client');
279
 
280
        $html = \html_writer::tag('p', get_string('setupfactor:securitykeyinfo', 'factor_webauthn'));
281
        $mform->addElement('static', 'devicenameinfo', '', $html);
282
 
283
        // Register security key.
284
        $mform->addElement('html', \html_writer::tag('p',
285
            get_string('setupfactor:instructionsregistersecuritykey', 'factor_webauthn'), ['class' => 'bold']));
286
 
287
        $registerbtn = \html_writer::tag('btn', get_string('register', 'factor_webauthn'), [
288
            'class' => 'btn btn-primary',
289
            'type' => 'button',
290
            'id' => 'factor_webauthn-register',
291
            'tabindex' => '0',
292
        ]);
293
        $mform->addElement('static', 'register', '', $registerbtn);
294
 
295
        $mform->addElement('hidden', 'response_input', '', ['id' => 'id_response_input']);
296
        $mform->setType('response_input', PARAM_RAW);
297
        $mform->addRule('response_input', get_string('required'), 'required', null, 'client');
298
 
299
        // Cross-platform: true if type internal is not allowed,
300
        // false if only internal is allowed,
301
        // null if internal and cross-platform is allowed.
302
        $types = explode(',', get_config('factor_webauthn', 'authenticatortypes'));
303
        $crossplatformattachment = null;
304
        if ((in_array('usb', $types) || in_array('nfc', $types) || in_array('ble', $types) || in_array('hybrid', $types)) &&
305
            !in_array('internal', $types)) {
306
            $crossplatformattachment = true;
307
        } else if (!in_array('usb', $types) && !in_array('nfc', $types) && !in_array('ble', $types) &&
308
            !in_array('hybrid', $types) && in_array('internal', $types)) {
309
            $crossplatformattachment = false;
310
        }
311
 
312
        $createargs = $this->webauthn->getCreateArgs($USER->id, $USER->username, fullname($USER), 20, false,
313
            $this->userverification, $crossplatformattachment);
314
 
315
        $PAGE->requires->js_call_amd('factor_webauthn/register', 'init', [json_encode($createargs)]);
316
 
317
        // Challenge is regenerated on form submission, at this point we aren't aware if the form is submitted for being
318
        // loaded for the first time, so we store the existing and new challenge.
319
        if (isset($SESSION->factor_webauthn_challenge_new)) {
320
            $SESSION->factor_webauthn_challenge = $SESSION->factor_webauthn_challenge_new;
321
        }
322
        $SESSION->factor_webauthn_challenge_new = $this->webauthn->getChallenge()->getHex();
323
 
324
        return $mform;
325
    }
326
 
327
    /**
328
     * WebAuthn Factor implementation.
329
     *
330
     * @param object $data
331
     * @return stdClass|null
332
     */
333
    public function setup_user_factor(object $data): stdClass|null {
334
        global $DB, $USER, $SESSION;
335
 
336
        if (!empty($data->webauthn_name) && !empty($data->response_input) && isset($SESSION->factor_webauthn_challenge)) {
337
            $post = json_decode($data->response_input, null, 512, JSON_THROW_ON_ERROR);
338
 
339
            $clientdata = base64_decode($post->clientDataJSON);
340
            $attestationobject = base64_decode($post->attestationObject);
341
            $challenge = ByteBuffer::fromHex($SESSION->factor_webauthn_challenge);
342
            unset($SESSION->factor_webauthn_challenge);
343
 
344
            $registration =
345
                $this->webauthn->processCreate($clientdata, $attestationobject, $challenge, $this->userverification === 'required',
346
                    true, false);
347
            $registration->credentialId = base64_encode($registration->credentialId);
348
            $registration->AAGUID = base64_encode($registration->AAGUID);
349
            unset($registration->certificate);
350
 
351
            $row = new \stdClass();
352
            $row->userid = $USER->id;
353
            $row->factor = $this->name;
354
            $row->label = $data->webauthn_name;
355
            $row->secret = json_encode($registration);
356
            $row->timecreated = time();
357
            $row->createdfromip = $USER->lastip;
358
            $row->timemodified = time();
359
            $row->lastverified = time();
360
            $row->revoked = 0;
361
 
362
            // Check if a record with this configuration already exists, warning the user accordingly.
363
            $record = $DB->get_record('tool_mfa', [
364
                'userid' => $row->userid,
365
                'secret' => $row->secret,
366
                'factor' => $row->factor,
367
            ], '*', IGNORE_MULTIPLE);
368
            if ($record) {
369
                \core\notification::warning(get_string('error:alreadyregistered', 'factor_webauthn'));
370
                return null;
371
            }
372
 
373
            $id = $DB->insert_record('tool_mfa', $row);
374
            $record = $DB->get_record('tool_mfa', array('id' => $id));
375
            $this->create_event_after_factor_setup($USER);
376
 
377
            return $record;
378
        }
379
        return null;
380
    }
381
 
382
    /**
383
     * WebAuthn Factor implementation with replacement of existing factor.
384
     *
385
     * @param stdClass $data The new factor data.
386
     * @param int $id The id of the factor to replace.
387
     * @return stdClass|null the factor record, or null.
388
     */
389
    public function replace_user_factor(stdClass $data, int $id): stdClass|null {
390
        global $DB, $USER;
391
 
392
        $oldrecord = $DB->get_record('tool_mfa', ['id' => $id]);
393
        $newrecord = null;
394
 
395
        // Ensure we have a valid existing record before setting the new one.
396
        if ($oldrecord) {
397
            $newrecord = $this->setup_user_factor($data);
398
        }
399
        // Ensure the new record was created before revoking the old.
400
        if ($newrecord) {
401
            $this->revoke_user_factor($id);
402
        } else {
403
            \core\notification::warning(get_string('error:couldnotreplace', 'tool_mfa'));
404
            return null;
405
        }
406
        $this->create_event_after_factor_setup($USER);
407
 
408
        return $newrecord ?? null;
409
    }
410
 
411
}