Proyectos de Subversion Moodle

Rev

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